commit 28e2772c1372e49e8b5f99e5d189d1a0383a612c Author: g7sim <30436307+g7sim@users.noreply.github.com> Date: Wed Sep 8 19:52:21 2021 +0200 1st diff --git a/.easymin/ignore_prefixes b/.easymin/ignore_prefixes new file mode 100644 index 0000000..c7e2b1a --- /dev/null +++ b/.easymin/ignore_prefixes @@ -0,0 +1,17 @@ +x_ +X_ +-- +data/_ +data/index.html +.git +xx_ +XX_ +.easymin +Boxfile +phpunit +phpunit.xml +.travis.yml +codecov.yml +composer.json +composer.lock +vendor/ diff --git a/.easymin/ignore_types b/.easymin/ignore_types new file mode 100644 index 0000000..3d9bd91 --- /dev/null +++ b/.easymin/ignore_types @@ -0,0 +1,5 @@ +.svn +.psd +.xcf +.exe +.pxm \ No newline at end of file diff --git a/.easymin/noshrink_paths b/.easymin/noshrink_paths new file mode 100644 index 0000000..1249e9b --- /dev/null +++ b/.easymin/noshrink_paths @@ -0,0 +1,2 @@ +/include/thirdparty/ckeditor +/include/thirdparty/elFinder diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d77719b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdda58e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +x_* +X_* +data/_* +xx_* +lock_admin +coverage.xml +composer.lock +/vendor/* diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..a16c72e --- /dev/null +++ b/.htaccess @@ -0,0 +1,5 @@ +AddType application/x-javascript .js +AddType text/css .css +AddType text/xml .xml + +AcceptPathInfo On \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..83b3e1d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: php + +jobs: + include: + - php: 5.6 + - php: 7.0 + - php: 7.1 + - php: 7.2 + - php: 7.3 + - php: 7.4 + - php: 7.2 + env: gp_data_type=.json + + +# phpunit 8 will result in an error +# https://www.reddit.com/r/PHPhelp/comments/apipqs/travis_specifying_phpunit_version/ +before_script: + - composer init -n + - composer require phpunit/phpunit "<8 >=4" + - composer require symfony/process + - composer require guzzlehttp/guzzle + - composer require phpunit/php-code-coverage "<7" # for phpunit compat + #- phpenv config-add phpunit/phpconfig.ini + +script: + - vendor/bin/phpunit + - php phpunit/ServerCombineCoverage.php + - bash <(curl -s https://codecov.io/bash) -s ./x_coverage # Submit coverage report to https://codecov.io diff --git a/Addon.ini b/Addon.ini new file mode 100644 index 0000000..22cef2c --- /dev/null +++ b/Addon.ini @@ -0,0 +1,5 @@ + + +Addon_Name = 'Typesetter Core' +Addon_Unique_ID = 40 +Addon_Version = 5.2-rc diff --git a/README.md b/README.md new file mode 100644 index 0000000..05e95ae --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ + +

+

Typesetter CMS

+

Open source CMS written in PHP focused on ease of use with true WYSIWYG editing and flat-file storage.

+ + +* [Typesetter Home](https://www.typesettercms.com) +* [Typesetter Download](https://www.typesettercms.com/Download) +* [Typesetter Demo](https://www.typesettercms.com/Demo) +* [Typesetter Documentation](https://www.typesettercms.com/Docs) +* [Typesetter Forum](https://www.typesettercms.com/Forum) +* [Typesetter Wiki](https://github.com/gtbu/Typesetter5.2/wiki) with more detailed instructions ! + + +## Requirements ## +* PHP 7.3+ (this fork only) + + +## Installation ## +1. Download the latest stable release of Typesetter from TypesetterCMS.com + +2. Upload the extracted contents to your server + +3. Using your web browser, navigate to the folder you just uploaded the unzipped contents to + +4. Complete the installation form and submit + +You can find more detailed installation information on [TypesetterCMS.com](https://www.typesettercms.com/Docs/Installation) + + +## Contribute ## +Submitting bug fixes and enhancements is easy: + +1. Log in to GitHub + +2. Fork the Typesetter Repository + * https://github.com/Typesetter/Typesetter + * Click "Fork" and you'll have your very own copy of the Typesetter source code at https://github.com/{your-username}/Typesetter + +3. Edit files within your fork. + This can be done directly on GitHub.com at https://github.com/{your-username}/Typesetter + +4. Submit a Pull Request (tell Typesetter about your changes) + * Click "Pull Request" + * Enter a Message that will go with your commit to be reviewed by core committers + * Click “Send Pull Request” + +### Multiple Pull Requests and Edits ### +When submitting pull requests, it is extremely helpful to isolate the changes you want included from other unrelated changes you may have made to your fork of Typesetter. The easiest way to accomplish this is to use a different branch for each pull request. There are a number of ways to create branches within your fork, but GitHub makes the process very easy: + +1. Start by finding the file you want to edit in Typesetter's code repository at https://github.com/Typesetter/Typesetter. +2. Once you have located the file, navigate to the code view and click "Edit". For example, if you want to change the /include/common.php file, the "Edit" button would appear on this page: https://github.com/Typesetter/Typesetter/blob/master/include/common.php +3. Now, edit the file as you like then click "Propose File Change" diff --git a/addons/--AntiSpamMath/Addon.ini b/addons/--AntiSpamMath/Addon.ini new file mode 100644 index 0000000..b274c1a --- /dev/null +++ b/addons/--AntiSpamMath/Addon.ini @@ -0,0 +1,34 @@ + +;Addon_Name +Addon_Name = 'Anti Spam Math' + +;Addon_Unique_ID +Addon_Unique_ID = 125 + +;Addon_Version +Addon_Version = 1.2 + +;min_gpeasy_version +min_gpeasy_version = 2.3.3 + +;A description about your addon, +; may contain some html:
,

,,,
,,,,,,,,, +About = 'Display a simple math equation on forms to verify submitters are humans and not spam bots.'; + +;file containing a list of the editable text +editable_text = 'Text.php' + +; Hook for form display +[AntiSpam_Form] + +script = 'AntiSpamMath.php' +class = 'AntiSpamMath' +method = 'Form' + +; Hook for verification of form +[AntiSpam_Check] + +script = 'AntiSpamMath.php' +class = 'AntiSpamMath' +method = 'Check' + diff --git a/addons/--AntiSpamMath/AntiSpamMath.php b/addons/--AntiSpamMath/AntiSpamMath.php new file mode 100644 index 0000000..662f223 --- /dev/null +++ b/addons/--AntiSpamMath/AntiSpamMath.php @@ -0,0 +1,82 @@ +'plus',2=>'minus',3=>'divided by',4=>'times'); + + function Form($html){ + $operator_key = array_rand($this->operators); + $operator = $this->operators[$operator_key]; + + $asm_1 = rand(1,10); + $asm_3 = rand(1,10); + if ($operator_key == 3) $asm_1 = $asm_1 * $asm_3; + + + $inputs = array(); + $inputs[] = ' '; + $inputs[] = ' '; + $inputs[] = ' '; + shuffle($inputs); + + ob_start(); + echo implode('',$inputs); + + echo ''; + echo $asm_1; + echo ' '; + echo gpOutput::GetAddonText($operator); + echo ' '; + echo $asm_3; + echo ' '; + echo gpOutput::GetAddonText('equals'); + echo ' '; + echo ''; + $html .= ob_get_clean(); + + return $html; + } + + function Check($passed){ + $message = gpOutput::SelectText('Sorry, your answer to the verification challenge was incorrect. Please try again.'); + + if( empty($_POST['asm_1']) || empty($_POST['asm_2']) || empty($_POST['asm_3']) ){ + message($message.' (1)'); + return false; + } + + $operator_key = $_POST['asm_2']; + if( !isset($this->operators[$operator_key]) ){ + message($message.' (2)'); + return false; + } + + switch($operator_key){ + case 1: + $result = $_POST['asm_1'] + $_POST['asm_3']; + break; + case 2: + $result = $_POST['asm_1'] - $_POST['asm_3']; + break; + case 3: + $result = $_POST['asm_1'] / $_POST['asm_3']; + break; + case 4: + $result = $_POST['asm_1'] * $_POST['asm_3']; + break; + } + + $compare = $_POST['asm_4']; + //message('result: '.$result.' vs submitted: '.$compare); + + if( $compare != $result ){ + message($message.' (3)'); + return false; + } + + //message('passed'); + + return $passed; + } + +} diff --git a/addons/--AntiSpamMath/Install_Check.php b/addons/--AntiSpamMath/Install_Check.php new file mode 100644 index 0000000..6427ac6 --- /dev/null +++ b/addons/--AntiSpamMath/Install_Check.php @@ -0,0 +1,20 @@ +Cannot install this addon, missing feature.

'; + return false; + } + */ + + return true; +} diff --git a/addons/--AntiSpamMath/Text.php b/addons/--AntiSpamMath/Text.php new file mode 100644 index 0000000..432cd27 --- /dev/null +++ b/addons/--AntiSpamMath/Text.php @@ -0,0 +1,13 @@ +AvailableClasses filter hook. See the code comments.'; + +;New hook for filtering of Availabe Classes +[AvailableClasses] +script = AvailableClasses.php +method = AvailableClassesExample::AvailableClasses + +;Hook for adding CSS, JS or meta tags to the page +[GetHead] +script = AvailableClasses.php +method = AvailableClassesExample::GetHead diff --git a/addons/--AvailableClasses/AvailableClasses.css b/addons/--AvailableClasses/AvailableClasses.css new file mode 100644 index 0000000..6b6dc85 --- /dev/null +++ b/addons/--AvailableClasses/AvailableClasses.css @@ -0,0 +1,8 @@ +.plugin-example-toggle { + background: black; + color: white; +} + +.plugin-example-option-1 { color: red; } +.plugin-example-option-2 { color: green; } +.plugin-example-option-3 { color: blue; } diff --git a/addons/--AvailableClasses/AvailableClasses.php b/addons/--AvailableClasses/AvailableClasses.php new file mode 100644 index 0000000..a79c5dd --- /dev/null +++ b/addons/--AvailableClasses/AvailableClasses.php @@ -0,0 +1,135 @@ + Configuration -> Classes + * + */ + static function AvailableClasses($classes){ + + // ONLY FOR USE IN A THEME + // exits if the page does not use this theme + /* + if( self::IsCurrentTheme == false ){ + return $classes; + } + */ + + // REPLACE ALL + // not recommended because it renders the Available Classes system settings useless + /* + $classes = array( + array( + 'names' => 'plugin-example-toggle', + 'desc' => 'A single CSS class entry defined via plugin filter hook', + ), + array( + 'names' => 'plugin-example-option-1 example-option-3 example-option-3', + 'desc' => 'A list of selectable classes entry defined via plugin filter hook', + ) + ); + */ + + // APPEND SINGLE ENTRY + // will add one definition at the end of the defined list + /* + $classes[] = array( + 'names' => 'plugin-example-toggle', + 'desc' => 'A single CSS class appended entry to preset classes via plugin filter hook', + ); + */ + + // APPEND MULTIPLE ENTRIES + // will add multiple definitions at the end of the defined list + /* + $append_these = array( + array( + 'names' => 'plugin-example-toggle', + 'desc' => 'A single CSS class entry appended via plugin filter hook', + ), + array( + 'names' => 'plugin-example-option-1 example-option-3 example-option-3', + 'desc' => 'A list of selectable classes appended via plugin filter hook', + ) + ); + $classes = array_merge($classes, $append_these); + */ + + // PREPEND SINGLE ENTRY + // will add one definition at the start of the defined list + /* + $append_this = array( + 'names' => 'plugin-example-toggle', + 'desc' => 'A single CSS class entry appended to preset classes via plugin filter hook', + ); + array_unshift($classes, $append_this); + */ + + // PREPEND MULTIPLE ENTRIES + // will add one (or more) definitions at the beginning of the defined list + // probably the most likely useful variant in plugins or themes + //* + $prepend_these = array( + array( + 'names' => 'plugin-example-toggle', + 'desc' => 'A single CSS class entry prepended via plugin filter hook', + ), + array( + 'names' => 'plugin-example-option-1 plugin-example-option-3 plugin-example-option-3', + 'desc' => 'A list of selectable classes entry prepended via plugin filter hook', + ) + ); + $classes = array_merge($prepend_these, $classes); + // */ + + + // MANDATORY + return $classes; + } + + + + /** + * + * Typesetter action hook + * + * when AvailaleClasses hook is used in a plugin, like right here, you might want to + * load the stylesheet(s) containing rules for your classname entries + * + */ + static function GetHead(){ + global $page, $addonRelativeCode, $addon_current_id, $addon_current_version; + // \gp\tool\Plugins::css('AvailableClasses.css'); // the stylesheet will be combined with other CSS (if 'combine CSS' is active in configuration) + \gp\tool\Plugins::css('AvailableClasses.css', false); // the stylesheet will NOT be combined with other CSS + } + + + + /** + * + * Custom method to check if current page uses our theme + * + * obviously only useful when AvailaleClasses hook is used + * in the context of a theme + * + * returns true or false + * + */ + static function IsCurrentTheme(){ + global $page, $gpLayouts, $config, $addonFolderName; + $theme_name = $config['addons'][$addonFolderName]['name']; + $layout = isset($page->TitleInfo['gpLayout']) ? $page->TitleInfo['gpLayout'] : 'default'; + $default_layout = $config['gpLayout']; + $current_theme_name = $layout == 'default' ? $gpLayouts[$default_layout]['name'] : $gpLayouts[$layout]['name']; + $is_current_theme = ($current_theme_name == $theme_name); + return $is_current_theme; + } + +} diff --git a/addons/--AvailableClasses/Install_Check.php b/addons/--AvailableClasses/Install_Check.php new file mode 100644 index 0000000..6427ac6 --- /dev/null +++ b/addons/--AvailableClasses/Install_Check.php @@ -0,0 +1,20 @@ +Cannot install this addon, missing feature.

'; + return false; + } + */ + + return true; +} diff --git a/addons/--Child_Thumbnails/Addon.ini b/addons/--Child_Thumbnails/Addon.ini new file mode 100644 index 0000000..798d07c --- /dev/null +++ b/addons/--Child_Thumbnails/Addon.ini @@ -0,0 +1,10 @@ +Addon_Name = 'Child Thumbnails' +Addon_Unique_ID = 238 +Addon_Version = 1.1 +min_gpeasy_version = 4.0 + +About = 'Display a list of icons for each child page'; + +[Gadget:Child_Thumbnails] +script = 'Child_Thumbnails.php' +class = 'Child_Thumbnails' diff --git a/addons/--Child_Thumbnails/Child_Thumbnails.php b/addons/--Child_Thumbnails/Child_Thumbnails.php new file mode 100644 index 0000000..2fa4258 --- /dev/null +++ b/addons/--Child_Thumbnails/Child_Thumbnails.php @@ -0,0 +1,126 @@ +gp_index]) ){ + return; + } + + $titles = common::Descendants($page->gp_index, $gp_menu); + $level = $gp_menu[$page->gp_index]['level']; + + echo '
    '; + foreach( $titles as $index ){ + + //only show children + $child_level = $gp_menu[$index]['level']; + if( $child_level != $level+1 ){ + continue; + } + + $title = array_search($index, $gp_index); + + //don't show if external link + if( !$title ){ + continue; + } + + if( !isset($gp_titles[$index]['vis']) ){ + $this->Child($title); + } + + } + echo '
'; + + } + + + /** + * Get The Image + * + */ + public function Child($title){ + global $dirPrefix, $addonRelativeCode, $addonPathCode; + + $content = $this->TitleContent($title); + + $img_pos = strpos($content,''; + echo '
'; + echo '' . $label .''; + echo '' . $label . ''; + echo ''; + echo ''; + } + + + /** + * Return the formatted content of the title + * + */ + public function TitleContent($title){ + $file = gpFiles::PageFile($title); + + $file_sections = $file_stats = array(); + + ob_start(); + require($file); + ob_get_clean(); + + if( !is_array($file_sections) ){ + return ''; + } + + //prevent infinite loops + foreach($file_sections as $key=>$val){ + if( $val['type'] == 'include' ){ + //dummy section instead of include + $file_sections[$key] = array ( + 'type' => 'text', + 'content' => '

Lorem ipsum

', + 'attributes' => array (), + ); + } + } + + if( !$file_sections ){ + return ''; + } + + $file_sections = array_values($file_sections); + + return section_content::Render($file_sections,$title,$file_stats); + } + +} diff --git a/addons/--Easy_Comments/Addon.ini b/addons/--Easy_Comments/Addon.ini new file mode 100644 index 0000000..20b8e06 --- /dev/null +++ b/addons/--Easy_Comments/Addon.ini @@ -0,0 +1,63 @@ + +;Addon_Name +Addon_Name = 'Easy Comments' + +;Addon_Unique_ID +Addon_Unique_ID = 114 + +;Addon_Version +Addon_Version = 1.2.0 + +;min_gpeasy_version +; needs 2.2.0.3 for fixes to ajax responses in gpEasy +min_gpeasy_version = 4.3.5 + + +;A description about your addon, +; may contain some html:
,

,,,
,,,,,,,,, +About = '

Add comments to any Typesetter page

'; + + +;file containing a list of the editable text +editable_text = 'Text.php' + + +; Theme Gadget (Optional) +; Define scripts that can output content to +[Gadget:Easy_Comments] + +;optional, relative to the addon directory +script = 'EasyComments_Gadget.php' + +;optional, relative to the plugin's data directory +;data = 'Gadget_Data.php' + +; optional +class = 'EasyComments_Gadget' + + +;Admin_links (Optional) +;Define scripts that are only accessible to administrators with appropriate permissions +[Admin_Link:Recent_Comments] + +;required +label = 'Recent Comments' + +;required relative to the addon directory +script = 'EasyComments_Admin.php' + +; optional +class = 'EasyComments_Admin' + +;Admin_links (Optional) +;Define scripts that are only accessible to administrators with appropriate permissions +[Admin_Link:Comments_Config] + +;required +label = 'Configuration' + +;required relative to the addon directory +script = 'EasyComments_Config.php' + +; optional +class = 'EasyComments_Config' diff --git a/addons/--Easy_Comments/EasyComments.php b/addons/--Easy_Comments/EasyComments.php new file mode 100644 index 0000000..058f3b1 --- /dev/null +++ b/addons/--Easy_Comments/EasyComments.php @@ -0,0 +1,236 @@ +current_title = $page->title; + $page->ajaxReplace = []; + + + $this->config_file = $addonPathData.'/config.php'; + $this->GetConfig(); + + // index is not required for all page displays + $this->index_file = $addonPathData.'/index.php'; + + //only available for pages with a gp_index + if( empty($page->gp_index) ){ + return; + } + + + $this->InitPage($page->gp_index); + } + + + /** + * Initialize page specific variables + * + */ + public function InitPage($index){ + global $gp_titles,$addonPathData; + + if( !isset($gp_titles[$index]) ){ + return; + } + + + $this->current_index = $index; + $this->comment_folder = $addonPathData.'/comments'; + + + $this->comment_data_file = $this->comment_folder.'/'.$this->current_index.'.gpjson'; + if( file_exists($this->comment_data_file) ){ + $content = file_get_contents($this->comment_data_file); + $this->comment_data = json_decode($content,true); + return; + } + + + // get data saved before v1.2 + //$this->comment_data_file = $this->comment_folder.'/'.$this->current_index.'.txt'; + $data_file = $this->comment_folder.'/'.$this->current_index.'.txt'; + if( file_exists($data_file) ){ + $content = file_get_contents($data_file); + $this->comment_data = unserialize($content); + } + } + + + /** + * Add Comment to index file + * + */ + public function UpdateIndex($rm_key=false){ + + $this->GetIndex(); + + + //update the information for the $current_index + unset($this->index['pages'][$this->current_index]); + if( count($this->comment_data) > 0){ + + $temp = end($this->comment_data); + $last_key = key($this->comment_data); + reset($this->comment_data); + + + $last_comment = array(); + $last_comment['abbr'] = substr($temp['comment'],0,100); + $last_comment['time'] = $temp['time']; + $last_comment['count'] = count($this->comment_data); + $last_comment['key'] = $last_key; + $last_comment['page'] = $this->current_index; + $last_comment['name'] = $temp['name']; + if( isset($temp['website']) ){ + $last_comment['website'] = $temp['website']; + } + + $this->index['pages'][$this->current_index] = $last_comment; + + + //if it's a new comment + if( $rm_key === false ){ + $this->index['recent'][] = $last_comment; + } + } + + + //remove from the recent comments base on current_index and comment time + if( $rm_key !== false ){ + foreach($this->index['recent'] as $i => $recent){ + if( ($recent['page'] == $this->current_index) && ($recent['key'] == $rm_key) ){ + unset($this->index['recent'][$i]); + } + } + } + + + //only keep the 20 most recent comments + while( count($this->index['recent']) > 20 ){ + array_shift($this->index['recent']); + } + + return $this->SaveIndex(); + } + + + public function SaveIndex(){ + return \gp\tool\Files::SaveData($this->index_file, 'index', $this->index); + } + + public function GetIndex(){ + + if( file_exists($this->index_file) ){ + $index = \gp\tool\Files::Get($this->index_file, 'index'); + } + + if( !isset($index['pages']) ){ + $index['pages'] = array(); + } + if( !isset($index['recent']) ){ + $index['recent'] = array(); + } + + $this->index = $index; + + return $index; + } + + + /** + * Save the comment data + * + */ + public function SaveCommentData(){ + global $langmessage; + + $text = json_encode($this->comment_data); + if( !\gp\tool\Files::Save($this->comment_data_file,$text) ){ + return false; + } + + return true; + } + + + /** + * Get the current configuration for Easy Comments + * + */ + public function GetConfig(){ + + $config = array(); + if( file_exists($this->config_file) ){ + require($this->config_file); + } + + $this->config = $config + $this->Defaults(); + } + + /** + * Return Easy Comments configuration defaults + * + */ + public function Defaults(){ + return array( + 'date_format'=>'n/j/Y', + 'commenter_website'=>'', + 'comment_captcha'=>false, + 'email'=>false + ); + } + + + +} diff --git a/addons/--Easy_Comments/EasyComments_Admin.php b/addons/--Easy_Comments/EasyComments_Admin.php new file mode 100644 index 0000000..20b0679 --- /dev/null +++ b/addons/--Easy_Comments/EasyComments_Admin.php @@ -0,0 +1,195 @@ +GetIndex(); + $cmd = \gp\tool::GetCommand(); + + if( isset($_REQUEST['pg']) ){ + $this->InitPage($_REQUEST['pg']); + + switch($cmd){ + case 'easy_comment_rm': + $this->CommentRm(); + return; + } + + } + + + $this->ShowAdmin(); + } + + + + + /** + * Show the default admin window that display recent comments + * + */ + public function ShowAdmin(){ + global $page; + + + echo '

Most Recent Comments

'; + + if( count($this->index['recent']) > 0 ){ + echo ''; + echo ''; + $recent = array_reverse($this->index['recent']); + foreach($recent as $comment){ + $this->CommentRow($comment['page'],$comment); + } + echo '
PageComment TimeCommenterCommentOptions
'; + }else{ + echo 'No comments to display'; + } + + + echo '
'; + + echo '

Recently Commented Pages

'; + + if( count($this->index['pages']) > 0 ){ + echo ''; + echo ''; + $pages = array_reverse($this->index['pages'],true); + foreach($pages as $page_key => $comment){ + $this->CommentRow($page_key,$comment); + } + echo '
PageComment TimeCommenterCommentOptions
'; + }else{ + echo '

'; + echo 'No comments to display'; + echo '

'; + } + + + } + + public function CommentRow($page_index,$comment){ + global $gp_index, $gp_titles, $langmessage; + + $key =& $comment['key']; + + echo ''; + echo ''; + $title = \gp\tool::IndexToTitle($page_index); + if( $title === false ){ + echo 'Deleted page'; + }else{ + $label = \gp\tool::GetLabelIndex($page_index); + echo \gp\tool::Link($title,$label); + } + echo ''; + echo date('D, j M Y H:i',$comment['time']); + echo ''; + if( !empty($comment['website']) ){ + echo '
'.$comment['name'].''; + }else{ + echo 'no website'; + echo $comment['name']; + } + echo ''; + echo $comment['abbr']; + echo ''; + + echo \gp\tool::Link('Admin_Recent_Comments',$langmessage['delete'],'cmd=easy_comment_rm&pg='.$page_index.'&i='.$key,' data-cmd="gpajax"'); + + echo ''; + + } + + + /** + * Prompt the administrator if they really want to remove the comment + * + */ + public function CommentRm(){ + global $page, $langmessage; + + $page->ajaxReplace = []; + + if( !isset($_REQUEST['i']) ){ + msg($langmessage['OOPS'].' (Invalid Request)'); + return false; + } + + if( !isset($this->comment_data[$_REQUEST['i']]) ){ + msg($langmessage['OOPS'].' (Invalid Request)'); + return false; + } + + $comment_key = $_REQUEST['i']; + $nonce_str = 'easy_comment_rm:'.count($this->comment_data).':'.$comment_key; + + //prompt for confirmation first + if( !isset($_POST['confirmed']) ){ + $this->CommentRm_Prompt(); + return true; + } + + if( !\gp\tool::verify_nonce($nonce_str,$_POST['nonce']) ){ + msg($langmessage['OOPS'].' (Invalid Nonce)'); + return false; + } + + + //remove from this page's comment data + unset($this->comment_data[$comment_key]); + if( !$this->SaveCommentData() ){ + msg($langmessage['OOPS'].' (Not Saved)'); + return false; + } + + + //update the index file + $this->UpdateIndex($comment_key); + + $class = '.easy_comment_'.$this->current_index.'_'.$comment_key; + $page->ajaxReplace[] = array('detach',$class); + $page->ajaxReplace[] = array('detach','.messages'); + + return true; + } + + public function CommentRm_Prompt(){ + global $page, $langmessage; + + $page->ajaxReplace = array(); + $del_comment = \gp\tool\Output::SelectText('Delete Comment'); + $nonce_str = 'easy_comment_rm:'.count($this->comment_data).':'.$_REQUEST['i']; + + ob_start(); + + echo '
'; + echo '
'; + echo ''; + echo \gp\tool\Output::SelectText('Are you sure you want to remove this comment?'); + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo '
'; + echo '
'; + + $message = ob_get_clean(); + msg($message); + } + + + + +} diff --git a/addons/--Easy_Comments/EasyComments_Config.php b/addons/--Easy_Comments/EasyComments_Config.php new file mode 100644 index 0000000..b462341 --- /dev/null +++ b/addons/--Easy_Comments/EasyComments_Config.php @@ -0,0 +1,142 @@ +SaveConfig(); + default: + $this->ShowConfig(); + break; + + } + } + + + /** + * Save posted configuration options + * + */ + public function SaveConfig(){ + global $langmessage; + + $format = htmlspecialchars($_POST['date_format']); + if( @date($format) ){ + $this->config['date_format'] = $format; + } + + $this->config['commenter_website'] = (string)$_POST['commenter_website']; + + if( isset($_POST['comment_captcha']) ){ + $this->config['comment_captcha'] = true; + }else{ + $this->config['comment_captcha'] = false; + } + + + if( !\gp\tool\Files::SaveData($this->config_file, 'config', $this->config) ){ + message($langmessage['OOPS']); + return false; + } + + message($langmessage['SAVED']); + return true; + } + + + /** + * Show EasyComments configuration options + * + */ + public function ShowConfig(){ + global $langmessage; + + $defaults = $this->Defaults(); + + $array = $_POST + $this->config; + + echo '

Easy Comments Configuration

'; + + echo '
'; + echo ''; + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo '
'; + echo 'Option'; + echo ''; + echo 'Value'; + echo ''; + echo 'Default'; + echo '
'; + echo 'Date Format'; + echo ' (About)'; + echo ''; + echo ''; + echo ''; + echo $defaults['date_format']; + echo '
'; + echo 'Commenter Website'; + echo ''; + echo ''; + echo ''; + echo 'Hide'; + echo '
'; + echo 'reCaptcha'; + echo ''; + + if( !\gp\tool\Recaptcha::isActive() ){ + $disabled = ' disabled="disabled" '; + }else{ + $disabled = ''; + } + + if( $array['comment_captcha'] ){ + echo ''; + }else{ + echo ''; + } + echo ''; + echo ''; + echo '
'; + echo ''; + echo ''; + echo ' '; + echo ' '; + echo ''; + echo '
'; + } + + +} diff --git a/addons/--Easy_Comments/EasyComments_Gadget.php b/addons/--Easy_Comments/EasyComments_Gadget.php new file mode 100644 index 0000000..a9ff12d --- /dev/null +++ b/addons/--Easy_Comments/EasyComments_Gadget.php @@ -0,0 +1,266 @@ +'; + echo '

Comments

'; + + if( !$this->current_index ){ + echo '

Comments are not available for this page

'; + echo '
'; + return; + } + + + $cmd = \gp\tool::GetCommand(); + + $comment_added = false; + switch($cmd){ + case 'easy_comment_add': + $comment_added = $this->CommentAdd(); + break; + } + + $this->ShowComments(); + + if( !$comment_added ){ + $this->CommentForm(); + } + echo '
'; + } + + + /** + * Save a user submitted comment + * + */ + public function CommentAdd(){ + global $langmessage; + + + // check the nonce + // includes the comment count so resubmissions won't work + if( !\gp\tool::verify_nonce('easy_comments:'.count($this->comment_data),$_POST['nonce'],true) ){ + $message = \gp\tool\Output::GetAddonText('Sorry, your comment was not saved.'); + msg($message); + return false; + } + + + //check captcha + if( $this->config['comment_captcha'] && \gp\tool\Recaptcha::isActive() ){ + + if( !\gp\tool\Recaptcha::Check() ){ + //recaptcha::check adds message on failure + return false; + } + } + + + if( empty($_POST['name']) ){ + $field = \gp\tool\Output::SelectText('Name'); + msg($langmessage['OOPS_REQUIRED'],$field); + return false; + } + + if( empty($_POST['comment']) ){ + $field = \gp\tool\Output::SelectText('Comment'); + msg($langmessage['OOPS_REQUIRED'],$field); + return false; + } + + + $temp = array(); + $temp['name'] = htmlspecialchars($_POST['name']); + $temp['comment'] = nl2br(strip_tags($_POST['comment'])); + $temp['time'] = time(); + + if( !empty($_POST['website']) && ($_POST['website'] !== 'http://') ){ + $website = $_POST['website']; + if( strpos($website,'://') === false ){ + $website = false; + } + if( $website ){ + $temp['website'] = $website; + } + } + + $index = $this->NewIndex(); + $this->comment_data[$index] = $temp; + + + //save to index file first + if( !$this->UpdateIndex() ){ + $message = \gp\tool\Output::GetAddonText('Sorry, your comment was not saved.'); + msg($message); + return false; + } + + + //then save actual comment + if( $this->SaveCommentData() ){ + $message = \gp\tool\Output::GetAddonText('Your comment has been saved.'); + msg($message); + return true; + }else{ + $message = \gp\tool\Output::GetAddonText('Sorry, your comment was not saved.'); + msg($message); + return false; + } + } + + + /** + * Show the comments for the current page + * + */ + public function ShowComments( ){ + global $langmessage; + + + echo '
'; + foreach($this->comment_data as $key => $comment){ + echo '
'; + echo '

'; + if( ($this->config['commenter_website'] == 'nofollow') && !empty($comment['website']) ){ + echo ''.$comment['name'].''; + }elseif( ($this->config['commenter_website'] == 'link') && !empty($comment['website']) ){ + echo ''.$comment['name'].''; + }else{ + echo ''.$comment['name'].''; + } + echo '   '; + echo ''; + echo date($this->config['date_format'],$comment['time']); + echo ''; + + + if( \gp\tool::LoggedIn() ){ + echo '   '; + echo \gp\tool::Link('Admin_Recent_Comments',$langmessage['delete'],'cmd=easy_comment_rm&i='.$key.'&pg='.$this->current_index,' data-cmd="gpajax"'); + } + + + echo '

'; + echo '

'; + echo $comment['comment']; + echo '

'; + echo '
'; + } + echo '
'; + } + + + + /** + * Generate a new comment index + * skip indexes that are just numeric + * + */ + public function NewIndex(){ + + $num_index = 0; + + /* prevent reusing old indexes */ + if( count($this->comment_data) > 0 ){ + end($this->comment_data); + $last_index = key($this->comment_data); + reset($this->comment_data); + $num_index = base_convert($last_index,36,10); + $num_index++; + } + + do{ + $index = base_convert($num_index,10,36); + $num_index++; + }while( is_numeric($index) || isset($this->comment_data[$index]) ); + + return $index; + } + + + /** + * Show the comment form + * + */ + public function CommentForm(){ + + + $_POST += array('name'=>'','website'=>'http://','comment'=>''); + + echo '
'; + echo '

'; + echo \gp\tool\Output::GetAddonText('Leave Comment'); + echo '

'; + + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if( !empty($this->config['commenter_website']) ){ + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + + + if( $this->config['comment_captcha'] && \gp\tool\Recaptcha::isActive() ){ + + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + + echo '
'; + echo '
'; + echo \gp\tool\Output::GetAddonText('Name'); + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo \gp\tool\Output::GetAddonText('Website'); + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo \gp\tool\Output::GetAddonText('Comment'); + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo \gp\tool\Output::GetAddonText('captcha'); + echo '
'; + \gp\tool\Recaptcha::Form(); + echo '
'; + echo ''; + + echo ''; + $html = ''; + echo \gp\tool\Output::GetAddonText('Add Comment',$html); + echo '
'; + echo ''; + echo '
'; + + } + +} diff --git a/addons/--Easy_Comments/Install_Check.php b/addons/--Easy_Comments/Install_Check.php new file mode 100644 index 0000000..6427ac6 --- /dev/null +++ b/addons/--Easy_Comments/Install_Check.php @@ -0,0 +1,20 @@ +Cannot install this addon, missing feature.

'; + return false; + } + */ + + return true; +} diff --git a/addons/--Easy_Comments/Text.php b/addons/--Easy_Comments/Text.php new file mode 100644 index 0000000..41c3ea3 --- /dev/null +++ b/addons/--Easy_Comments/Text.php @@ -0,0 +1,16 @@ +pagetype; + if( $page_type != 'admin_display' ){ + $page->head_js[] = $addonRelativeCode . '/jquery-highlight/jquery.highlight.min.js'; + $page->css_user[] = $addonRelativeCode . '/Highlight.css'; + $page->head_js[] = $addonRelativeCode . '/Highlight.js'; + } + } + +} diff --git a/addons/--Highlight_Search_Results/jquery-highlight/jquery.highlight.js b/addons/--Highlight_Search_Results/jquery-highlight/jquery.highlight.js new file mode 100644 index 0000000..e628bb4 --- /dev/null +++ b/addons/--Highlight_Search_Results/jquery-highlight/jquery.highlight.js @@ -0,0 +1,109 @@ +/* + * jQuery Highlight plugin 3.4.0 + * + * https://www.npmjs.com/package/jquery-highlight#api + * + * Based on highlight v3 by Johann Burkard + * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html + * + * Code a little bit refactored and cleaned (in my humble opinion). + * Most important changes: + * - has an option to highlight only entire words (wordsOnly - false by default), + * - has an option to be case sensitive (caseSensitive - false by default) + * - highlight element tag and class names can be specified in options + * + * Usage: + * // wrap every occurrance of text 'lorem' in content + * // with (default options) + * $('#content').highlight('lorem'); + * + * // search for and highlight more terms at once + * // so you can save some time on traversing DOM + * $('#content').highlight(['lorem', 'ipsum']); + * $('#content').highlight('lorem ipsum'); + * + * // search only for entire word 'lorem' + * $('#content').highlight('lorem', { wordsOnly: true }); + * + * // don't ignore case during search of term 'lorem' + * $('#content').highlight('lorem', { caseSensitive: true }); + * + * // wrap every occurrance of term 'ipsum' in content + * // with + * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); + * + * // remove default highlight + * $('#content').unhighlight(); + * + * // remove custom highlight + * $('#content').unhighlight({ element: 'em', className: 'important' }); + * + * + * Copyright (c) 2009 Bartek Szopka + * + * Licensed under MIT license. + * + */ + +jQuery.extend({ + highlight: function (node, re, nodeName, className) { + if (node.nodeType === 3) { + var match = node.data.match(re); + if (match) { + var highlight = document.createElement(nodeName || 'span'); + highlight.className = className || 'highlight'; + var wordNode = node.splitText(match.index); + wordNode.splitText(match[0].length); + var wordClone = wordNode.cloneNode(true); + highlight.appendChild(wordClone); + wordNode.parentNode.replaceChild(highlight, wordNode); + return 1; //skip added node in parent + } + } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children + !/(script|style)/i.test(node.tagName) && // ignore script and style nodes + !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted + for (var i = 0; i < node.childNodes.length; i++) { + i += jQuery.highlight(node.childNodes[i], re, nodeName, className); + } + } + return 0; + } +}); + +jQuery.fn.unhighlight = function (options) { + var settings = { className: 'highlight', element: 'span' }; + jQuery.extend(settings, options); + + return this.find(settings.element + "." + settings.className).each(function () { + var parent = this.parentNode; + parent.replaceChild(this.firstChild, this); + parent.normalize(); + }).end(); +}; + +jQuery.fn.highlight = function (words, options) { + var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; + jQuery.extend(settings, options); + + if (words.constructor === String) { + words = [words]; + } + words = jQuery.grep(words, function(word, i){ + return word != ''; + }); + words = jQuery.map(words, function(word, i) { + return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }); + if (words.length == 0) { return this; }; + + var flag = settings.caseSensitive ? "" : "i"; + var pattern = "(" + words.join("|") + ")"; + if (settings.wordsOnly) { + pattern = "\\b" + pattern + "\\b"; + } + var re = new RegExp(pattern, flag); + + return this.each(function () { + jQuery.highlight(this, re, settings.element, settings.className); + }); +}; diff --git a/addons/--Highlight_Search_Results/jquery-highlight/jquery.highlight.min.js b/addons/--Highlight_Search_Results/jquery-highlight/jquery.highlight.min.js new file mode 100644 index 0000000..5001d41 --- /dev/null +++ b/addons/--Highlight_Search_Results/jquery-highlight/jquery.highlight.min.js @@ -0,0 +1,2 @@ +/* jQuery Highlight plugin 3.4.0 */ +jQuery.extend({highlight:function(e,t,n,i){if(3===e.nodeType){var r=e.data.match(t);if(r){var a=document.createElement(n||"span");a.className=i||"highlight";var h=e.splitText(r.index);h.splitText(r[0].length);var s=h.cloneNode(!0);return a.appendChild(s),h.parentNode.replaceChild(a,h),1}}else if(1===e.nodeType&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&(e.tagName!==n.toUpperCase()||e.className!==i))for(var l=0;l 'Check back home!', + 'priority' => 80, // optional: an integer > 100 will overtake working drafts notifications + 'id' => 'Example Note Check back home!', // needs to be unique amongst other notes for filtering + + 'action' => '' . \CMS_READABLE_DOMAIN . '', + ), + + array( + 'label' => 'GitHub', + 'priority' => 30, + 'id' => 'Example Note GitHub', + 'action' => 'View issues', + ), + + array( + 'label' => 'This Plugin', + 'priority' => 110, + 'id' => 'Uninstall this plugin', + 'action' => \gp\tool::Link( + 'Admin/Addons', + $langmessage['uninstall'], + 'cmd=uninstall&addon=Notifications-Example', + array( + 'data-cmd' => 'gpabox', + 'title' => $langmessage['uninstall'] + ) + ), + ), + ); + + $notifications->Add('Plugin Example',$items,'#c594e8','#000'); + + /* + $notifications['example'] = array( // unique identifier string. using 'drafts', 'private_pages' or 'updates' would overwrite possible existing notes + + 'title' => 'Plugin Example', // required: will automatically be checked against $langmessage + 'badge_bg' => '#c594e8', // optional: badge background color + 'badge_color' => '#000', // optional: badge text color + + + ), + ); + */ + + return $notifications; + } + +} diff --git a/addons/--Plugin_Examples/01_Map.php b/addons/--Plugin_Examples/01_Map.php new file mode 100644 index 0000000..07589d0 --- /dev/null +++ b/addons/--Plugin_Examples/01_Map.php @@ -0,0 +1,31 @@ + + $page->head .= ''; + $page->head .= ''; + $page->head .= ''; + + + //html contents of the page + echo '

Display a Google Map With Directions

'; + + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + echo '
'; + + //plugin example navigation + gpPlugin::incl('navigation.php'); + PluginExampleNavigation(); + } +} + + diff --git a/addons/--Plugin_Examples/02_Ajax.php b/addons/--Plugin_Examples/02_Ajax.php new file mode 100644 index 0000000..75f3d06 --- /dev/null +++ b/addons/--Plugin_Examples/02_Ajax.php @@ -0,0 +1,42 @@ +head_js[] = $addonRelativeCode.'static/02_script.js'; + $page->admin_js = true; + + //get request parameters and execute any commands + $string = ''; + if( isset($_REQUEST['string']) ){ + $string = $_REQUEST['string']; + } + $cmd = common::GetCommand(); + switch($cmd){ + case 'randomstring': + $string = common::RandomString(10); + break; + } + + //display the form + echo '

Example Ajax Requests

'; + echo '
'; + echo 'Text: '; + echo ' '; + echo common::Link($page->title,'Get Random String','cmd=randomstring','data-cmd="gpajax"'); + echo '
'; + + + //output the $_REQUEST variable + echo '

Request

'; + echo pre($_REQUEST); + + + //plugin example navigation + gpPlugin::incl('navigation.php'); + PluginExampleNavigation(); + } +} diff --git a/addons/--Plugin_Examples/Addon.ini b/addons/--Plugin_Examples/Addon.ini new file mode 100644 index 0000000..52a633c --- /dev/null +++ b/addons/--Plugin_Examples/Addon.ini @@ -0,0 +1,30 @@ + +;Addon_Name +Addon_Name = 'Plugin Examples' + +;Addon_Unique_ID +Addon_Unique_ID = 160 + +;Addon_Version +Addon_Version = 1.1 + +;min_gpeasy_version +min_gpeasy_version = 2.3.3 + +;A description about your addon, +; may contain some html:
,

,,,
,,,,,,,,, +About = 'A collection of examples to demonstrate plugin functionality'; + +;Map Example +[Special_Link:Example_Map] +label = 'Example_Map' +script = '01_Map.php' +class = 'Example_Map' + +;Ajax Example +[Special_Link:Example_Ajax] +label = 'Example_Ajax' +script = '02_Ajax.php' +class = 'Example_Ajax' + + diff --git a/addons/--Plugin_Examples/navigation.php b/addons/--Plugin_Examples/navigation.php new file mode 100644 index 0000000..d1965dd --- /dev/null +++ b/addons/--Plugin_Examples/navigation.php @@ -0,0 +1,23 @@ +All Examples'; + echo '

    '; + foreach($examples as $slug => $label){ + if( $page->gp_index == $slug ){ + echo '
  1. '.$label.'
  2. '; + }else{ + echo '
  3. '.common::Link($slug,$label).'
  4. '; + } + } + echo '
'; +} diff --git a/addons/--Plugin_Examples/static/01_script.js b/addons/--Plugin_Examples/static/01_script.js new file mode 100644 index 0000000..92bd2a8 --- /dev/null +++ b/addons/--Plugin_Examples/static/01_script.js @@ -0,0 +1,40 @@ + var directionDisplay; + var directionsService = new google.maps.DirectionsService(); + var map; + + $(function(){ + directionsDisplay = new google.maps.DirectionsRenderer(); + var chicago = new google.maps.LatLng(50.903315,13.67583); + var myOptions = { + zoom:17, + mapTypeId: google.maps.MapTypeId.ROADMAP, + center: chicago + } + map = new google.maps.Map(document.getElementById("map_canvas"), myOptions); + directionsDisplay.setMap(map); + directionsDisplay.setPanel(document.getElementById("directionsPanel")); + var chicago = new google.maps.Marker({ + position: chicago, + map: map, + title:"Sportpark Dippoldiswalde", + zIndex: 1 + }); + + $('#calc_route_button').click(calcRoute); + + }); + + + function calcRoute(){ + var start = document.getElementById("map_address").value; + var request = { + origin:start, + destination:"50.903315,13.67583", + travelMode: google.maps.DirectionsTravelMode.DRIVING + }; + directionsService.route(request, function(response, status){ + if (status == google.maps.DirectionsStatus.OK){ + directionsDisplay.setDirections(response); + } + }); + } diff --git a/addons/--Plugin_Examples/static/02_script.js b/addons/--Plugin_Examples/static/02_script.js new file mode 100644 index 0000000..85b0f63 --- /dev/null +++ b/addons/--Plugin_Examples/static/02_script.js @@ -0,0 +1,5 @@ +gplinks.refresh_content = function(rel,evt){ + evt.preventDefault(); + var href = jPrep(this.href)+'&cmd=refresh'; + $.getJSON(href,ajaxResponse); +} diff --git a/addons/--ReplaceContentVars/Addon.ini b/addons/--ReplaceContentVars/Addon.ini new file mode 100644 index 0000000..04337cc --- /dev/null +++ b/addons/--ReplaceContentVars/Addon.ini @@ -0,0 +1,8 @@ +Addon_Name = 'Replace Content Variables' +Addon_Version = 1.0-b1 +min_gpeasy_version = 5.1-b1 +About = 'For Typesetter 5.1-b1+ Customize replacable variables. See Sections.php on Github.'; + +[ReplaceContentVars] +script = Replace.php +method = ReplaceCVs::ReplaceContentVars diff --git a/addons/--ReplaceContentVars/Replace.php b/addons/--ReplaceContentVars/Replace.php new file mode 100644 index 0000000..13701d7 --- /dev/null +++ b/addons/--ReplaceContentVars/Replace.php @@ -0,0 +1,21 @@ +John Doe'; // use $myNameIs in text content + + // you may also unset or change preset variables + // $vars['fileModTime'] = 'i won’t tell!'; + + return $vars; + } + +} diff --git a/addons/--Shadow_Box_Gallery/Addon.ini b/addons/--Shadow_Box_Gallery/Addon.ini new file mode 100644 index 0000000..9577fcb --- /dev/null +++ b/addons/--Shadow_Box_Gallery/Addon.ini @@ -0,0 +1,44 @@ + +;Addon_Name +Addon_Name = 'Shadow Box Gallery' + +;Addon_Unique_ID +Addon_Unique_ID = 231 + +;Addon_Version +Addon_Version = 1.1 + +;min_gpeasy_version +min_gpeasy_version = 4.0rc2 + + +;A description about your addon, +; may contain some html:
,

,,,
,,,,,,,,, +About = 'A dynamic gallery for file sections and extra content' + + +[SectionTypes] +script = Slideshow.php +method = SlideshowB::SectionTypes + +[SectionToContent] +script = Slideshow.php +method = SlideshowB::SectionToContent + +[GetDefaultContent] +script = Slideshow.php +method = SlideshowB::DefaultContent + +[SaveSection] +script = Slideshow.php +method = SlideshowB::SaveSection + +[GenerateContent_Admin] +script = Slideshow.php +method = SlideshowB::GenerateContent_Admin + +[InlineEdit_Scripts] +script = Slideshow.php +method = SlideshowB::InlineEdit_Scripts + + diff --git a/addons/--Shadow_Box_Gallery/Install_Check.php b/addons/--Shadow_Box_Gallery/Install_Check.php new file mode 100644 index 0000000..7b0d5af --- /dev/null +++ b/addons/--Shadow_Box_Gallery/Install_Check.php @@ -0,0 +1,20 @@ +Cannot install this addon, missing feature.

'; + return false; + } + */ + + return true; +} diff --git a/addons/--Shadow_Box_Gallery/Slideshow.php b/addons/--Shadow_Box_Gallery/Slideshow.php new file mode 100644 index 0000000..a00ed72 --- /dev/null +++ b/addons/--Shadow_Box_Gallery/Slideshow.php @@ -0,0 +1,212 @@ + 'Shadow Box Gallery'); + + return $section_types; + } + + /** + * @static + */ + static function SectionToContent($section_data){ + global $dataDir; + + if( $section_data['type'] != self::ContentKey() ){ + return $section_data; + } + + + SlideshowB::AddComponents(); + + return $section_data; + } + + static function GenerateContent($section_data){ + $section_data += array('images'=>array()); + + $icons = $first_image = $first_caption = ''; + foreach($section_data['images'] as $i => $img){ + $caption =& $section_data['captions'][$i]; + + $hash = $size_a = false; + $attr = ''; + + if( empty($first_image) ){ + $hash = 1; + $first_caption = $caption; + $first_image = ''; + $first_image .= ''; + $first_image .= ''; + } + + if( $hash ){ + $attr = ' data-hash="'.$hash.'" class="slideshowb_icon_'.$hash.'"'; + } + + $thumb_path = common::ThumbnailPath($img); + $icons .= '
  • '; + $icons .= ''; + $icons .= ''; + $icons .= ''; + $icons .= '
    '.$caption.'
    '; + $icons .= '
  • '; + } + + ob_start(); + + + $class = 'slideshowb_wrap'; + if( $section_data['auto_start'] ){ + $class .= ' start'; + } + $attr = ' data-speed="5000"'; + if( isset($section_data['interval_speed']) && is_numeric($section_data['interval_speed']) ){ + $attr = ' data-speed="'.$section_data['interval_speed'].'"'; + } + + echo '
    '; + echo '
    '; + echo $first_image; + echo '
    '; + echo '
    '; + echo $first_caption.' '; + echo '
    '; + echo '
      '; + echo $icons; + echo '
    '; + echo '
    '; + + return ob_get_clean(); + } + + + + + /** + * @static + */ + static function DefaultContent($default_content,$type){ + if( $type != self::ContentKey() ){ + return $default_content; + } + + ob_start(); + echo '
    '; + echo '
    '; + //echo $images; + echo '
    '; + echo '
    '; + //echo $caption.' '; + echo '
    '; + echo '
      '; + //echo $icons; + echo '
    '; + echo '
    '; + return ob_get_clean(); + } + + /** + * @static + */ + static function SaveSection($return,$section,$type){ + global $page; + if( $type != self::ContentKey() ){ + return $return; + } + + $_POST += array('auto_start'=>''); + $page->file_sections[$section]['auto_start'] = ($_POST['auto_start'] == 'true'); + $page->file_sections[$section]['images'] = $_REQUEST['images']; + $page->file_sections[$section]['captions'] = $_REQUEST['captions']; + $page->file_sections[$section]['interval_speed'] = 5000; + if( isset($_POST['interval_speed']) && is_numeric($_POST['interval_speed']) ){ + $page->file_sections[$section]['interval_speed'] = $_POST['interval_speed']; + } + $page->file_sections[$section]['content'] = self::GenerateContent($page->file_sections[$section]); + + return true; + } + + + /** + * Make sure the .js and .css is available to admins + * + * @static + * + */ + static function GenerateContent_Admin(){ + global $addonFolderName, $page, $addonRelativeCode; + $page->head_script .= "\nvar SLIDESHOW_BASE = '".$addonRelativeCode."';\n"; + SlideshowB::AddComponents(); + } + + + static function AddComponents(){ + global $addonFolderName, $page, $addonRelativeCode; + static $done = false; + + if( $done ) return; + + $page->admin_js = true; //loads main.js + $page->head_js[] = '/data/_addoncode/'.$addonFolderName.'/slideshow.js'; + $page->css_user[] = '/data/_addoncode/'.$addonFolderName.'/slideshow.css'; + + $done = true; + } + + + /** + * + * + */ + static function InlineEdit_Scripts($scripts,$type){ + global $addonRelativeCode; + if( $type !== self::ContentKey() ){ + return $scripts; + } + + $scripts[] = '/include/js/inline_edit/inline_editing.js'; + $scripts[] = '/include/js/inline_edit/image_common.js'; + $scripts[] = '/include/js/inline_edit/gallery_edit_202.js'; + $scripts[] = $addonRelativeCode.'/gallery_options.js'; + $scripts[] = '/include/js/jquery.auto_upload.js'; + + + return $scripts; + } +} + + + + + + + + diff --git a/addons/--Shadow_Box_Gallery/gallery_options.js b/addons/--Shadow_Box_Gallery/gallery_options.js new file mode 100644 index 0000000..a186095 --- /dev/null +++ b/addons/--Shadow_Box_Gallery/gallery_options.js @@ -0,0 +1,39 @@ + +$.extend(gp_editor,{ + + sortable_area_sel: '.slideshowb_icons ul', + img_name: 'slideshowb_img', + img_rel: '', + auto_start: true, + intervalSpeed : function(){}, + + + /** + * Size Changes + * + */ + heightChanged : function(){ + $('.gp_editing .slideshowb_images').stop(true,true).delay(800).animate({'height':this.value,'line-height':this.value+'px'}); + }, + + + /** + * Update the caption if the current image is the active image + * + */ + updateCaption: function(current_image,text){ + var $li = $(current_image); + if( $li.index() === 0 ){ + $li.closest('.slideshowb_wrap').find('.slideshowb_caption').html(text+' '); + } + }, + + removeImage: function(image){ + var $li = $(image); + if( $li.index() === 0 ){ + $li.closest('.slideshowb_wrap').find('.slideshowb_icons li').eq(1).find('a').click(); + } + } + +}); + diff --git a/addons/--Shadow_Box_Gallery/imgs/gtk-media-next-ltr.png b/addons/--Shadow_Box_Gallery/imgs/gtk-media-next-ltr.png new file mode 100644 index 0000000..f250c77 Binary files /dev/null and b/addons/--Shadow_Box_Gallery/imgs/gtk-media-next-ltr.png differ diff --git a/addons/--Shadow_Box_Gallery/imgs/gtk-media-next-rtl.png b/addons/--Shadow_Box_Gallery/imgs/gtk-media-next-rtl.png new file mode 100644 index 0000000..b47cb71 Binary files /dev/null and b/addons/--Shadow_Box_Gallery/imgs/gtk-media-next-rtl.png differ diff --git a/addons/--Shadow_Box_Gallery/imgs/gtk-media-pause.png b/addons/--Shadow_Box_Gallery/imgs/gtk-media-pause.png new file mode 100644 index 0000000..38937ec Binary files /dev/null and b/addons/--Shadow_Box_Gallery/imgs/gtk-media-pause.png differ diff --git a/addons/--Shadow_Box_Gallery/imgs/gtk-media-play-ltr.png b/addons/--Shadow_Box_Gallery/imgs/gtk-media-play-ltr.png new file mode 100644 index 0000000..6b860d9 Binary files /dev/null and b/addons/--Shadow_Box_Gallery/imgs/gtk-media-play-ltr.png differ diff --git a/addons/--Shadow_Box_Gallery/imgs/loader64.gif b/addons/--Shadow_Box_Gallery/imgs/loader64.gif new file mode 100644 index 0000000..216d3ad Binary files /dev/null and b/addons/--Shadow_Box_Gallery/imgs/loader64.gif differ diff --git a/addons/--Shadow_Box_Gallery/slideshow.css b/addons/--Shadow_Box_Gallery/slideshow.css new file mode 100644 index 0000000..7a133c5 --- /dev/null +++ b/addons/--Shadow_Box_Gallery/slideshow.css @@ -0,0 +1,136 @@ +/** + * Provider images + * + */ +.slideshowb_wrap{ + position:relative; + border:1px solid #ddd; + border-radius:3px; + overflow:hidden; + width:85%; + margin:20px auto; + background:#f7f7f7; + background:linear-gradient(to bottom, #fcfcfc, #f5f5f5 30px); + box-shadow: -5px -5px 5px -5px #fff inset, 5px 5px 5px -5px #ddd inset; + padding:30px 0; + } +.slideshowb_images{ + position:relative; + height:400px; + line-height:400px; + overflow:hidden; + white-space:nowrap; + display:block; + padding:2px; + } +.slideshowb_images a{ + position:absolute; + display:block; + width:100%; + text-align:center; + white-space:nowrap; + overflow:hidden; + cursor:pointer; + /* left:100%; */ + + /* background: url(../imgs/loaderA64.gif) 50% 50% no-repeat; */ + background: url(../../../../include/imgs/loader64.gif) 50% 50% no-repeat; + height:inherit; + line-height:inherit; + z-index:0; + outline:0 none; + vertical-align:middle; + } +.slideshowb_images a.loaded{ + background:none; + } +/* +.slideshowb_images a.current{ + z-index:10; + } + */ +.slideshowb_images a.first{ + left:0; + } +.slideshowb_images img{ + max-height:100%; + max-width:90%; + vertical-align:middle; + } + +.slideshowb_icons{ + position:absolute; + top:0; + bottom:0; + left:-45px; + width: 80px; + display:inline-block; + overflow:hidden; + line-height:100%; + z-index:100; + transition:left .5s linear, background .5s linear; + } + +.gp_editing .slideshowb_icons, +.slideshowb_icons:hover{ + left:0; + background:#fff; + background:rgba(255,255,255,0.8); + } + +.slideshowb_icons ul{ + position:relative; + padding:5px 0 0 0; + list-style:none; + margin:0; + } +.slideshowb_icons li{ + margin:0; + padding:0; + } +.slideshowb_icons a{ + display:block !important; + cursor:pointer; + outline:0 none; + padding:5px 10px; + } +.slideshowb_icons img{ + margin:0; + overflow:hidden; + display:inline-block; + border-radius:30px; + box-shadow:0 0 1px #aaa; + height:60px; + width:60px; + } +.slideshowb_icons .caption{ + display:none; + } + + +.slideshowb_caption{ + position:absolute; + bottom:0; + left:0; + right:0; + line-height:30px; + height:30px; + text-align:center; + font-size:12px; + text-shadow:1px 1px 0 #fff; + color:#444; + padding:0 90px; + + } +.slideshowb_icons span{ + position:absolute; + top:5px; + height:68px; + width:68px; + left:5px; + border:1px solid rgba(0,0,0,0.15); + border-radius:34px; + background:#fff; + background:rgba(255,255,255,0.9); + } + diff --git a/addons/--Shadow_Box_Gallery/slideshow.js b/addons/--Shadow_Box_Gallery/slideshow.js new file mode 100644 index 0000000..ebc1397 --- /dev/null +++ b/addons/--Shadow_Box_Gallery/slideshow.js @@ -0,0 +1,212 @@ + +$(function(){ + + $(document).on('keyup',function(evt){ + + switch(evt.which){ + case 39: + var $slideshow = GetSlideshow(); + NextImg( $slideshow ); + break; + case 37: + var $slideshow = GetSlideshow(); + PrevImg( $slideshow ); + break; + } + }); + + function GetSlideshow(){ + var $slideshows = $('.slideshowb_wrap'); + if( $slideshows.length == 1 ){ + return $slideshows.eq(0); + } + var $window = $(window); + var w_top = $window.scrollTop(); + var w_bottom = $window.height() + w_top; + + + var max_overlap = 0, slideshow_index = 0; + $.each($slideshows,function(i,j){ + var $slideshow = $(j); + var s_top = $slideshow.offset().top; + var s_bottom = $slideshow.height() + s_top; + + if( s_top > w_top ){ + overlap = w_bottom - s_top; + }else{ + overlap = s_bottom - w_top; + } + if( overlap > max_overlap ){ + max_overlap = overlap; + slideshow_index = i; + } + }); + return $slideshows.eq(slideshow_index); + } + + //var $images = $('.slideshowb_images'); + + $gp.links.slideshowb_img = function(evt){ + evt.preventDefault(); + var $this = $(this); + var $slideshow = $this.closest('.slideshowb_wrap'); + LoadImg( $slideshow, $this, true ); + } + + + $gp.links.slideshowb_next = function(evt){ + var $slideshow = $(this).closest('.slideshowb_wrap'); + evt.preventDefault(); + + + //previous or next + var half = ($slideshow.width()/2) + $slideshow.offset().left; + if( evt.pageX > half ){ + NextImg($slideshow); + }else{ + PrevImg($slideshow); + } + } + + function NextImg($slideshow){ + var next = $slideshow.find('.slideshowb_icons li:eq(1) a'); + LoadImg( $slideshow, next, true ); + } + + function PrevImg($slideshow){ + var prev = $slideshow.find('.slideshowb_icons li:last a'); + LoadImg( $slideshow, prev, false ); + } + + function LoadImg( $slideshow, lnk, next ){ + + if( !lnk.length ){ + return; + } + + var curr = $slideshow.find('.slideshowb_icons a:first'); + var $images = $slideshow.find('.slideshowb_images'); + var left = $images.outerWidth(); + + var hash = Hash( lnk ); + if( curr.length ){ + var curr_hash = Hash( curr ); + if( curr_hash == hash ){ + return; + } + } + + var new_span = ImgSpan( $images, lnk ); + var curr_span = ImgSpan( $images, curr ); + + //scroll current image + if( !next ){ + left *= -1; + } + //new_span.css('left',left).animate({'left':0}); + new_span.animate({'left':left},{duration:0}).animate({'left':0}); + curr_span.animate( + {'left':-(left*2)} + ); + + + //scroll icons + var icon = $slideshow.find('a.slideshowb_icon_'+hash).parent(); + var icon_wrap = $slideshow.find('.slideshowb_icons > ul'); + + + if( next ){ + var pos = icon.position(); + icon_wrap.animate( + {'top': -(pos.top)} + ,{ + complete: function(){ + + icon_wrap.animate({'top':0},{duration:0,complete:function(){ + var prev = icon.prevUntil().detach().get().reverse(); + $.each(prev,function(){ + $(this).hide().appendTo( icon_wrap ).fadeIn(); + }); + } + }); + } + } + ); + }else{ + icon.prependTo( icon_wrap ); + icon_wrap.animate({'top':-80},{duration:0}); + icon_wrap.animate({'top':0}); + } + + + + $slideshow.find('.slideshowb_caption').html(lnk.attr('title')+' '); + + //load next image + var next = $slideshow.find('.slideshowb_icons li:eq(1) a'); + if( next.length != 0 ){ + ImgSpan( $images, next ); + } + } + + function ImgSpan( $images, lnk ){ + + var hash = Hash( lnk ); + var new_span = $images.find('a.slideshowb_img_'+hash); + + if( new_span.length == 0 ){ + + var href = lnk.attr('href'); + var new_span = $('').appendTo( $images ); + + var $img = $('').load(function(){ + $(this).parent().addClass('loaded'); + }).attr('src',href).appendTo(new_span); + + } + + return new_span; + } + + function Hash( lnk ){ + var hash = lnk.data('hash'); + if( hash ){ + return hash; + } + + do{ + hash = Math.round(Math.random()*100); + classname = 'slideshowb_icon_'+hash; + }while( $('.'+classname).length > 0 ); + + lnk.data('hash',hash); + lnk.addClass(classname); + return hash; + } + + //auto start + $('.slideshowb_wrap.start').each(function(){ + var $slideshow = $(this); + var speed = $slideshow.data('speed') || 5000; + + window.setInterval(function(){ + if( !$slideshow.hasClass('hover') ){ + NextImg( $slideshow ); + } + },speed); + + //cancel on mouseover + $slideshow.on('mouseenter',function(){ + $slideshow.addClass('hover'); + }).on('mouseleave',function(){ + $slideshow.removeClass('hover'); + }); + + + }); + + + +}); + + diff --git a/addons/--SimilarTitles-Example/Addon.ini b/addons/--SimilarTitles-Example/Addon.ini new file mode 100644 index 0000000..9ea5f5c --- /dev/null +++ b/addons/--SimilarTitles-Example/Addon.ini @@ -0,0 +1,9 @@ +Addon_Name = 'Similar Titles Example' +;Addon_Unique_ID = ??? +Addon_Version = 1.0 +min_gpeasy_version = 5.1.1-b1 +About = 'Plugin that illustrates the use of the SimilarTitles plugin filter hook. By removing certain pages from the passed $similar_titles array, they will be excluded from automatic redirection and will not show as suggested links on the \'Missing\' page anymore. To only prevent auto-redirection change the \'percent\' key value to be lower than the threshold value set in configuration.' + +[SimilarTitles] +script = SimilarTitles_Example.php +method = SimilarTitles_Example::SimilarTitles diff --git a/addons/--SimilarTitles-Example/SimilarTitles_Example.php b/addons/--SimilarTitles-Example/SimilarTitles_Example.php new file mode 100644 index 0000000..95ab9f6 --- /dev/null +++ b/addons/--SimilarTitles-Example/SimilarTitles_Example.php @@ -0,0 +1,78 @@ + $title ){ + $blacklist[] = $index; + } + //*/ + + + // Blacklist Example 2: remove arbitrary pages by their index + /*/ <-- remove the * to uncomment this code block + $blacklist = ['a', 'special_site_map']; + //*/ + + + // Blacklist Example 3: remove all special pages + /*/ <-- remove the * to uncomment this code block + foreach( $gp_index as $title => $index ){ + if( strpos($index, 'special_') === 0 ){ + $blacklist[] = $index; + } + } + //*/ + + + // Blacklist Example 4: remove all pages using the robots metatag with 'noindex' + /*/ <-- remove the * to uncomment this code block + foreach( $gp_titles as $index => $data ){ + if( isset($data['rel']) && strpos($data['rel'], 'noindex') !== false ){ + $blacklist[] = $index; + } + } + //*/ + + + // Only prevent auto-redirection for a certain page + // This example changes the percent value of the Contact page (if included) to zero: + /*/ <-- remove the * to uncomment this code block + if( isset($similar_titles['special_contact']) ){ + $similar_titles['special_contact']['percent'] = 0; + } + //*/ + + + // filter(remove) blacklisted pages + foreach( $similar_titles as $index => $similar ){ + if( in_array($index, $blacklist) ){ + unset($similar_titles[$index]); + } + } + + + // uncomment the following line for debugging + // msg('SimilarTitles Example plugin. AFTER: $similar_titles = ' . pre($similar_titles)); + + return $similar_titles; + } + +} diff --git a/addons/--Simple_Slideshow/Addon.ini b/addons/--Simple_Slideshow/Addon.ini new file mode 100644 index 0000000..c7eb267 --- /dev/null +++ b/addons/--Simple_Slideshow/Addon.ini @@ -0,0 +1,42 @@ + +;Addon_Name +Addon_Name = 'Simple Slideshow' + +;Addon_Unique_ID +Addon_Unique_ID = 87 + +;Addon_Version +Addon_Version = 1.3.3 + +;min_gpeasy_version +min_gpeasy_version = 4.0 + + +;A description about your addon, +; may contain some html:
    ,

    ,,,
    ,,,,,,,,, +About = 'Simple Slideshow is an alternative to the default Gallery datatype' + + +[SectionTypes] +script = Slideshow.php +method = SimpleSlideshow::SectionTypes + +[SectionToContent] +script = Slideshow.php +method = SimpleSlideshow::SectionToContent + +[GetDefaultContent] +script = Slideshow.php +method = SimpleSlideshow::DefaultContent + +[SaveSection] +script = Slideshow.php +method = SimpleSlideshow::SaveSection + +[GenerateContent_Admin] +script = Slideshow.php +method = SimpleSlideshow::GenerateContent_Admin + +[InlineEdit_Scripts] +script = Slideshow.php +method = SimpleSlideshow::InlineEdit_Scripts diff --git a/addons/--Simple_Slideshow/Install_Check.php b/addons/--Simple_Slideshow/Install_Check.php new file mode 100644 index 0000000..7b0d5af --- /dev/null +++ b/addons/--Simple_Slideshow/Install_Check.php @@ -0,0 +1,20 @@ +Cannot install this addon, missing feature.

    '; + return false; + } + */ + + return true; +} diff --git a/addons/--Simple_Slideshow/Slideshow.php b/addons/--Simple_Slideshow/Slideshow.php new file mode 100644 index 0000000..31c3569 --- /dev/null +++ b/addons/--Simple_Slideshow/Slideshow.php @@ -0,0 +1,262 @@ +'3000'); + + SimpleSlideshow::AddComponents(); + + + //gpEasy 4.0rc3+ + if( isset($section_data['full_content']) ){ + return $section_data; + } + + + //pre gpEasy 4.0rc3 + + //get the first image + $first_image = ''; + $first_caption = ''; + + //we could show the first caption + // - we'd have to make sure the html is valid first + // - convert > into > etc + $pattern = '#' + .''.$first_caption.'' + .''; + } + + + $controls = '
    ' + .'' + .'' + .'' + .'
    '; + + $section_data['content'] = '
    ' + .'
    ' + .'
    ' + .$controls + .'
    ' + .$first_image + .'
    ' + .'
    ' + .'
    ' + .$section_data['content'] + .'
    '; + + + return $section_data; + } + + + + + /** + * @static + */ + static function DefaultContent($default_content,$type){ + global $langmessage; + if( $type != 'simple_slide' ){ + return $default_content; + } + + $content = '
    '; + + + //gpEasy 4.0rc3+ + if( defined('gpversion') && version_compare(gpversion,'4.0rc3','<') ){ + return $content; + } + + $section = array(); + + $section['content'] = $content; + $section['auto_start'] = false; + $section['interval_speed'] = 3000; + return $section; + + } + + /** + * @static + * + */ + static function SaveSection($return,$section,$type){ + global $page; + if( $type != 'simple_slide' ){ + return $return; + } + + if( !array_key_exists('interval_speed', $_POST) ){ + $page->SaveSection_Text($section); + return true; + } + + $_POST += array('auto_start'=>'','images'=>array(),'interval_speed'=>''); + if( !is_numeric($_POST['interval_speed']) ){ + $_POST['interval_speed'] = '3000'; + } + + $page->file_sections[$section]['full_content'] = true; + $page->file_sections[$section]['interval_speed'] = $_POST['interval_speed']; + $page->file_sections[$section]['auto_start'] = ($_POST['auto_start'] == 'true'); + $page->file_sections[$section]['content'] = self::FromPost(); + + return true; + } + + //gpEasy 4.0rc3 + static function FromPost(){ + + //each image + $indicators = $first_image = ''; + foreach($_POST['images'] as $i => $img){ + if( empty($img) ){ + continue; + } + $caption = trim($_POST['captions'][$i]); + + if( empty($first_image) ){ + $first_image = '' + .''.htmlspecialchars($caption).'' + .''; + } + + + //indicators + $thumb_path = common::ThumbnailPath($img); + $indicators .= '
  • ' + .'' + .'' + .'' //for saving/editing of captions + .'
  • '; + } + + ob_start(); + + + $class = 'slideshow_area'; + if( $_POST['auto_start'] == 'true' ){ + $class .= ' start'; + } + $attr = ' data-speed="1000"'; + if( isset($_POST['interval_speed']) && is_numeric($_POST['interval_speed']) ){ + $attr = ' data-speed="'.$_POST['interval_speed'].'"'; + } + + echo '
    '; + echo '
    '; + echo '
    '; + echo '
    '; + echo ''; + echo ''; + echo ''; + echo '
    '; + echo '
    '; + echo $first_image; + echo '
    '; + echo '
    '; + echo '
    '; + + echo '
    '; + echo '
      '; + echo $indicators; + echo '
    '; + echo '
    '; + echo '
    '; + + return ob_get_clean(); + } + + + + /** + * Make sure the .js and .css is available to admins + * + * @static + * + */ + static function GenerateContent_Admin(){ + global $addonFolderName, $page, $addonRelativeCode; + $page->head_script .= "\nvar SLIDESHOW_BASE = '".$addonRelativeCode."';\n"; + SimpleSlideshow::AddComponents(); + + } + + + static function AddComponents(){ + global $addonFolderName, $page, $addonRelativeCode; + static $done = false; + + if( $done ) return; + + $page->admin_js = true; //loads main.js + $page->head_js[] = '/data/_addoncode/'.$addonFolderName.'/slideshow.js'; + $page->css_user[] = '/data/_addoncode/'.$addonFolderName.'/slideshow.css'; + + $done = true; + } + + + /** + * + * + */ + static function InlineEdit_Scripts($scripts,$type){ + global $addonRelativeCode; + + if( $type !== 'simple_slide' ){ + return $scripts; + } + + $scripts[] = '/include/js/inline_edit/inline_editing.js'; + $scripts[] = '/include/js/inline_edit/image_common.js'; + $scripts[] = '/include/js/inline_edit/gallery_edit_202.js'; + $scripts[] = $addonRelativeCode.'/gallery_options.js'; + $scripts[] = '/include/js/jquery.auto_upload.js'; + + + return $scripts; + } + +} + + + + + + + + diff --git a/addons/--Simple_Slideshow/gallery_options.js b/addons/--Simple_Slideshow/gallery_options.js new file mode 100644 index 0000000..352a8d0 --- /dev/null +++ b/addons/--Simple_Slideshow/gallery_options.js @@ -0,0 +1,14 @@ + +$.extend(gp_editor,{ + + sortable_area_sel : '.gp_slideshow', + img_name : 'gp_slideshow', + img_rel : '', + auto_start : true, + intervalSpeed : function(){}, + updateCaption : function(current_image, caption){ + $(current_image).find('a').attr('title',caption); + } + +}); + diff --git a/addons/--Simple_Slideshow/imgs/gtk-media-next-ltr.png b/addons/--Simple_Slideshow/imgs/gtk-media-next-ltr.png new file mode 100644 index 0000000..f250c77 Binary files /dev/null and b/addons/--Simple_Slideshow/imgs/gtk-media-next-ltr.png differ diff --git a/addons/--Simple_Slideshow/imgs/gtk-media-next-rtl.png b/addons/--Simple_Slideshow/imgs/gtk-media-next-rtl.png new file mode 100644 index 0000000..b47cb71 Binary files /dev/null and b/addons/--Simple_Slideshow/imgs/gtk-media-next-rtl.png differ diff --git a/addons/--Simple_Slideshow/imgs/gtk-media-pause.png b/addons/--Simple_Slideshow/imgs/gtk-media-pause.png new file mode 100644 index 0000000..38937ec Binary files /dev/null and b/addons/--Simple_Slideshow/imgs/gtk-media-pause.png differ diff --git a/addons/--Simple_Slideshow/imgs/gtk-media-play-ltr.png b/addons/--Simple_Slideshow/imgs/gtk-media-play-ltr.png new file mode 100644 index 0000000..6b860d9 Binary files /dev/null and b/addons/--Simple_Slideshow/imgs/gtk-media-play-ltr.png differ diff --git a/addons/--Simple_Slideshow/imgs/loader64.gif b/addons/--Simple_Slideshow/imgs/loader64.gif new file mode 100644 index 0000000..216d3ad Binary files /dev/null and b/addons/--Simple_Slideshow/imgs/loader64.gif differ diff --git a/addons/--Simple_Slideshow/slideshow.css b/addons/--Simple_Slideshow/slideshow.css new file mode 100644 index 0000000..42f32d6 --- /dev/null +++ b/addons/--Simple_Slideshow/slideshow.css @@ -0,0 +1,196 @@ + + +.slideshow-container{ + position:relative; + display:block; + position:relative; + width:100%; +} + +a.slideshow_slide{ + display: block; + /* position: absolute; */ + width:100%; + text-align:center; + } + +a.slideshow_slide img{ + position:relative; + border: 1px solid #ccc; + padding:3px; + } +* html a.slideshow_slide img{ + filter:inherit; + opacity:inherit; + } + +.caption-container{ + text-align:center; + } + +.loader{ + position:absolute; + top:0; + left:0; + height:100%; + width:100%; + z-index:1; + background:url('imgs/loader64.gif') 50% 50% no-repeat; + } +.loaded .loader{ + display:none; + } + +.gp_slide_cntrls{ + position:absolute; + top:2px; + width:100%; + z-index:2; + text-align:center; + display:none; + } +.hover .gp_slide_cntrls{ + display:block; + } + +.gp_slide_cntrls span{ + display:inline-block; + background-color:#fff; + padding:0 9px; + height:26px; + + filter:alpha(opacity=95); + -moz-opacity:0.95; + -khtml-opacity: 0.95; + opacity: 0.95; + + border:1px solid #ccc; + -moz-border-radius: 7px; + -o-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + + } + +.gp_slide_cntrls a{ + text-decoration:none; + font-size:11px; + font-weight:bold; + padding:5px 5px; + display:inline-block; + color:#333; + + height:16px; + width:16px; + } + +.gp_slide_play_pause{ + background:url('imgs/gtk-media-play-ltr.png') 50% 50% no-repeat; + } +.gp_slide_play{ + background:url('imgs/gtk-media-pause.png') 50% 50% no-repeat; + } +.gp_slide_next{ + background:url('imgs/gtk-media-next-ltr.png') 50% 50% no-repeat; + } +.gp_slide_prev{ + background:url('imgs/gtk-media-next-rtl.png') 50% 50% no-repeat; + } + + +/* thumbnails */ +/* + + + + + + +*/ + +/* thumbnails new */ +.gp_slide_thumbs{ + float:right; + position:relative; + left:-50%; + text-align:left; +} +.gp_slide_thumbs ul.gp_slideshow{ + list-style:none; + position:relative; + left:50%; + margin: 20px 0 20px 0; + padding: 0; +} + +.gp_slide_thumbs li{float:left;position:relative;}/* ie needs position:relative here*/ + +ul.gp_slideshow > li{ + padding: 0; + margin: 5px 10px 5px 0; + list-style: none; +} + +ul.gp_slideshow > li > a{ + float:left; + padding: 2px; + border: 1px solid #ccc; + height:75px; + width:75px; +} + +ul.gp_slideshow .caption{ + display:none; + } +ul.gp_slideshow > li.current > a{ + border-color: #000; +} +ul.gp_slideshow > li > a:focus { + outline: none; +} +ul.gp_slideshow > li > a img{ + height:75px; + width:75px; + } +ul.gp_slideshow > li.selected > a img { + border: none; + display: block; +} +.gp_slide_thumbs .gp_drag_box{ + height:75px; + width:75px; + } + + + + +/* CSS for captions above images + * the padding-top should be coordinated with the amount of area needed for the captions + */ +/* +.slideshow_area{ + position:relative; + padding-top:20px; + } +.caption-container{ + position:absolute; + top:0; + width:100%; + } +*/ + + + +/* for captions on the side + +Widths will need to be coordinated with the theme and images used + +.slideshow-container{ + float:right; + width:700px; + } +.caption-container{ + float:left; + width:200px; + } +*/ diff --git a/addons/--Simple_Slideshow/slideshow.js b/addons/--Simple_Slideshow/slideshow.js new file mode 100644 index 0000000..b480884 --- /dev/null +++ b/addons/--Simple_Slideshow/slideshow.js @@ -0,0 +1,217 @@ + + +$(function(){ + + var gp_slideshow = {}; + gp_slideshow.ImgSelect = function(a){ + var container, file_container, $anchor, href; + + if( !a ) return; + + $anchor = $(a); + file_container = $anchor.closest('.slideshow_area'); + container = file_container.find('.slideshow-container'); + + container.removeClass('loaded'); + + //change the thumbnail current class + var thumb_li = $anchor.parent(); + thumb_li.siblings().removeClass('current'); + thumb_li.addClass('current'); + + NewImg($anchor,true); + + function NewImg(source_link,onload){ + var img, hash; + + if( !source_link.attr('href') ) return; + + + var hash = source_link.data('hash'); + if( !hash ){ + hash = Hash(); + source_link.data('hash',hash); + } + + var lnk = container.find('#'+hash); + + //link not found, create it + if( lnk.length ){ + lnk.attr('title',source_link.attr('title')); + if( onload ){ + window.setTimeout(function(){ + loaded(lnk); + },100); + } + return; + } + + lnk = $('') + .hide() + .appendTo(container) + .attr('title',source_link.attr('title')); + img = $('').appendTo(lnk); + + + if( onload ){ + img.load(function(){ + //allow browser to get image size + window.setTimeout(function(){ + loaded(lnk); + },100); + }); + } + + //setting the src value needs to be after onload function is added + img.attr('src',source_link.attr('href')); + } + + + function loaded(lnk){ + + container.addClass('loaded'); + + var visible = container.find('a.slideshow_slide:visible'); + if( visible.attr('id') != lnk.attr('id') ){ + + //fade the current image + visible.stop(true,true).fadeOut(); + + //adjust container height + var h = lnk.outerHeight(); + container.stop(true,true).animate({'height':h}); + + //show the new image + lnk.css({'position':'absolute'}).stop(true,true).fadeIn(); + } + + //always make sure the caption is correct since the caption for the first image isn't automatic + file_container.find('.caption-container').html(lnk.attr('title')); + + //preload load next + var next = thumb_li.next().children(':first'); + if( next.length > 0 ) NewImg(next,false); + } + + } + + function Hash(){ + do{ + var hash = Math.round(Math.random()*10000); + }while( $('#'+hash).length > 0 ); + return hash; + } + + + + $gp.links.gp_slideshow_next = function(evt){ + evt.preventDefault(); + Next( $(this) ); + } + + function Next( $this ){ + + var thumbs = $this.closest('.slideshow_area').find('.gp_slideshow'); + if( thumbs.length == 0 ) return; + + var current = thumbs.children('li.current'); + var next_li = false; + if( current.length > 0 ){ + next_li = current.next(); + } + if( !next_li || next_li.length == 0 ){ + next_li = thumbs.children(':first'); + } + gp_slideshow.ImgSelect(next_li.children(':first').get(0)); + } + + $gp.links.gp_slideshow_prev = function(evt){ + evt.preventDefault(); + var thumbs = $(this).closest('.slideshow_area').find('.gp_slideshow'); + if( thumbs.length == 0 ) return; + + var current = thumbs.children('li.current'); + var prev_li = false; + if( current.length > 0 ){ + prev_li = current.prev(); + } + if( !prev_li || prev_li.length == 0 ){ + prev_li = thumbs.children(':last'); + } + gp_slideshow.ImgSelect(prev_li.children(':first').get(0)); + } + + $gp.links.gp_slideshow_play = function(evt){ + evt.preventDefault(); + + var interval = false; + var container = $(this).closest('.slideshow_area'); + var play_pause = container.find('.gp_slide_play_pause'); + if( play_pause.hasClass('gp_slide_play') ){ + //change to pause + play_pause.removeClass('gp_slide_play'); + }else{ + + var speed = container.data('speed') || 3000; + + //change to play + play_pause.addClass('gp_slide_play'); + interval = window.setInterval(function(){ + PlayNext(); + },speed); + } + + function PlayNext(){ + //no longer playing + if( !play_pause.hasClass('gp_slide_play') ){ + window.clearInterval(interval); + return; + } + Next( play_pause ); + } + } + + + $gp.links.gp_slideshow = function(evt){ + evt.preventDefault(); + gp_slideshow.ImgSelect(this); + } + + + $('ul.gp_slideshow').each(function(){ + var timeout = false; + function clear(){ + if( timeout ) + window.clearTimeout(timeout); + } + + //init containers + var container = $(this).closest('.slideshow_area'); + var html = container.find('.gp_nosave'); + var cntrls = html.find('.gp_slide_cntrls'); + + $(html).mousemove(function(){ + cntrls.stop(true,true).fadeIn(); + clear(); + timeout = window.setTimeout(function(){ + cntrls.stop(true,true).fadeOut('slow'); + },1500); + }).mouseleave(function(){ + cntrls.stop(true,true).fadeOut(); + }); + + + //first image + var hash = Hash(); + var first_img = $(this).find('li:first > a').data('hash',hash); + container.find('.first_image').attr('id',hash); + gp_slideshow.ImgSelect(first_img.get(0)); + + //auto start + if( container.hasClass('start') ){ + container.find('.gp_slide_play_pause').click(); + } + }); + + +}); diff --git a/addons/--Syntax_Highlighting/Addon.ini b/addons/--Syntax_Highlighting/Addon.ini new file mode 100644 index 0000000..2606340 --- /dev/null +++ b/addons/--Syntax_Highlighting/Addon.ini @@ -0,0 +1,34 @@ + +;Addon_Name +Addon_Name = 'Syntax Highlighter' + +;Addon_Unique_ID +Addon_Unique_ID = 227 + +;Addon_Version +Addon_Version = 1.0 + +;min_gpeasy_version +min_gpeasy_version = 3.6 + +;A description about your addon, +; may contain some html:
    ,

    ,,,
    ,,,,,,,,, +About = 'Add code syntax highlighting to any of your pages'; + + +[CKEditorPlugins] +script = 'HighlighterPlugin.php' +method = 'HighlighterPlugin::CKEditorPlugins' + +[HeadContent] +script = 'HighlighterPlugin.php' +method = 'HighlighterPlugin::CheckContent' + + + +;Admin_links (Optional) +;Define scripts that are only accessible to administrators with appropriate permissions +[Admin_Link:Admin_HighlighterSettings] +label = 'Syntax Highlighter Settings' +script = 'Admin.php' +class = 'HighlighterSettings' diff --git a/addons/--Syntax_Highlighting/Admin.php b/addons/--Syntax_Highlighting/Admin.php new file mode 100644 index 0000000..20d0f0e --- /dev/null +++ b/addons/--Syntax_Highlighting/Admin.php @@ -0,0 +1,94 @@ +config = gpPlugin::GetConfig(); + $this->config += array('theme'=>'default'); + + $this->themes = array( + 'default' => 'Default', + 'django' => 'Django', + 'eclipse' => 'Eclipse', + 'emacs' => 'Emacs', + 'fadetogrey' => 'Fade to Grey', + 'midnight' => 'Midnight', + 'rdark' => 'RDark', + 'none' => '[None]', + ); + + $this->themes = gpPlugin::Filter('syntaxhighlighter_themes', array( $this->themes ) ); + + $cmd = common::GetCommand(); + switch($cmd){ + case 'save'; + $this->Save(); + break; + } + + $this->ShowForm(); + } + + function Save(){ + global $langmessage; + + $theme =& $_POST['theme']; + if( isset($this->themes[$theme]) ){ + $this->config['theme'] = $theme; + } + + if( gpPlugin::SaveConfig($this->config) ){ + message($langmessage['SAVED']); + }else{ + message($langmessage['OOPS']); + } + } + + function ShowForm(){ + global $langmessage; + + if( count($_POST) ){ + $values = $_POST; + }else{ + $values = $this->config; + } + + echo '

    Syntax Highlighter Settings

    '; + + echo '
    '; + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo '
    '.$langmessage['options'].' 
    Color Theme'; + echo ''; + echo '
    '; + echo ''; + echo ''; + echo ''; + echo '
    '; + echo '
    '; + } + + +} + + diff --git a/addons/--Syntax_Highlighting/HighlighterPlugin.php b/addons/--Syntax_Highlighting/HighlighterPlugin.php new file mode 100644 index 0000000..a4a0495 --- /dev/null +++ b/addons/--Syntax_Highlighting/HighlighterPlugin.php @@ -0,0 +1,110 @@ +.. php... + * Add the appropriate js and css files + * + */ + static function CheckContent(){ + global $page, $addonRelativeCode; + + $content = ob_get_contents(); + + $avail_brushes['css'] = 'shBrushCss.js'; + $avail_brushes['diff'] = 'shBrushDiff.js'; + $avail_brushes['ini'] = 'shBrushIni.js'; + $avail_brushes['jscript'] = 'shBrushJScript.js'; + $avail_brushes['php'] = 'shBrushPhp.js'; + $avail_brushes['plain'] = 'shBrushPlain.js'; + $avail_brushes['sql'] = 'shBrushSql.js'; + $avail_brushes['xml'] = 'shBrushXml.js'; + + $brushes = array(); + + preg_match_all('#]*>#',$content,$matches); + if( !count($matches) ){ + return; + } + foreach($matches[0] as $match){ + preg_match('#class=[\'"]([^\'"]+)[\'"]#',$match,$classes); + if( !isset($classes[1]) ){ + continue; + } + + preg_match('#brush:([^;\'"]+)[;"\']?#',$match,$type); + if( !isset($type[1]) ){ + continue; + } + + $type = strtolower(trim($type[1])); + + if( !isset($avail_brushes[$type]) ){ + continue; + } + + $brushes[] = $avail_brushes[$type]; + } + + if( !count($brushes) ){ + return; + } + + $config = gpPlugin::GetConfig(); + $theme =& $config['theme']; + + $page->head .= "\n\n"; + $page->head .= ''."\n"; + + $css_file = 'shThemeDefault.css'; + switch($theme){ + case 'django': + $css_file = 'shThemeDjango.css'; + break; + case 'eclipse': + $css_file = 'shThemeEclipse.css'; + break; + case 'emacs': + $css_file = 'shThemeEmacs.css'; + break; + case 'fadetogrey': + $css_file = 'shThemeFadeToGrey.css'; + break; + case 'midnight': + $css_file = 'shThemeMidnight.css'; + break; + case 'rdark': + $css_file = 'shThemeRDark.css'; + break; + case 'none': + $css_file = false; + break; + } + if( $css_file ){ + $page->head .= ''."\n"; + } + + + $page->head .= ''."\n"; + foreach($brushes as $brush){ + $page->head .= ''."\n"; + } + $page->jQueryCode .= "\nSyntaxHighlighter.all();\n"; + } +} diff --git a/addons/--Syntax_Highlighting/Install_Check.php b/addons/--Syntax_Highlighting/Install_Check.php new file mode 100644 index 0000000..6427ac6 --- /dev/null +++ b/addons/--Syntax_Highlighting/Install_Check.php @@ -0,0 +1,20 @@ +Cannot install this addon, missing feature.

    '; + return false; + } + */ + + return true; +} diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/dialogs/syntaxhighlight.js b/addons/--Syntax_Highlighting/syntaxhighlight/dialogs/syntaxhighlight.js new file mode 100644 index 0000000..f734273 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/dialogs/syntaxhighlight.js @@ -0,0 +1,373 @@ +CKEDITOR.dialog.add( 'syntaxhighlightDialog', function( editor ) { + var a=function(f) { + f=f.replace(/
    /g,"\n"); + f=f.replace(/&/g,"&"); + f=f.replace(/</g,"<"); + f=f.replace(/>/g,">"); + f=f.replace(/"/g,'"'); + return f + }; + var e=function(f) { + var f=new Object(); + f.hideGutter=false; + f.hideControls=false; + f.collapse=false; + f.showColumns=false; + f.noWrap=false; + f.firstLineChecked=false; + f.firstLine=0; + f.highlightChecked=false; + f.highlight=null; + f.lang=null; + f.code=''; + return f + }; + var b=function(i) { + var h=e(); + if(i) { + if(i.indexOf('brush')>-1) { + var g=/brush:[ ]{0,1}(\w*)/.exec(i); + if(g!=null&&g.length>0) { + h.lang=g[1].replace(/^\s+|\s+$/g,'') + } + } + if(i.indexOf('gutter')>-1) { + h.hideGutter=true + } + if(i.indexOf('toolbar')>-1) { + h.hideControls=true + } + if(i.indexOf('collapse')>-1) { + h.collapse=true + } + if(i.indexOf('first-line')>-1) { + var g=/first-line:[ ]{0,1}([0-9]{1,4})/.exec(i); + if(g!=null&&g.length>0&&g[1]>1) { + h.firstLineChecked=true; + h.firstLine=g[1] + } + } + if(i.indexOf('highlight')>-1) { + if(i.match(/highlight:[ ]{0,1}\[[0-9]+(,[0-9]+)*\]/)) { + var f=/highlight:[ ]{0,1}\[(.*)\]/.exec(i); + if(f!=null&&f.length>0) { + h.highlightChecked=true; + h.highlight=f[1] + } + } + } + if(i.indexOf('ruler')>-1) { + h.showColumns=true + } + if(i.indexOf('wrap-lines')>-1) { + h.noWrap=true + } + } + return h + }; + var d=function(g) { + var f='brush:'+g.lang+';'; + if(g.hideGutter) { + f+='gutter:false;' + } + if(g.hideControls) { + f+='toolbar:false;' + } + if(g.collapse) { + f+='collapse:true;' + } + if(g.showColumns) { + f+='ruler:true;' + } + if(g.noWrap) { + f+='wrap-lines:false;' + } + if(g.firstLineChecked&&g.firstLine>1) { + f+='first-line:'+g.firstLine+';' + } + if(g.highlightChecked&&g.highlight!='') { + f+='highlight: ['+g.highlight.replace(/\s/gi,'')+'];' + } + return f + }; + return { + title : editor.lang.syntaxhighlight.title, + minWidth : 500, + minHeight : 400, + contents : [ + { + id : 'source', + label : editor.lang.syntaxhighlight.sourceTab, + accessKey : 'S', + elements : [ + { + type : 'vbox', + children : [ + { + id : 'cmbLang', + type : 'select', + labelLayout : 'horizontal', + label : editor.lang.syntaxhighlight.langLbl, + 'default' : 'java', + widths : ['25%','75%'], + items : [ + //['Bash (Shell)','bash'], + //['C#','csharp'], + //['C++','cpp'], + ['CSS','css'], + //['Delphi','delphi'], + ['Diff','diff'], + //['Groovy','groovy'], + ['Javascript','jscript'], + //['Java','java'], + //['Java FX','javafx'], + //['Perl','perl'], + ['PHP','php'], + ['Plain (Text)','plain'], + //['Python','python'], + //['Ruby','ruby'], + //['Scala','scala'], + ['SQL','sql'], + //['VB','vb'], + ['XML/XHTML','xml'] + ], + setup : function(f) { + if(f.lang) { + this.setValue(f.lang) + } + }, + commit : function(f) { + f.lang=this.getValue() + } + } + ] + }, + { + type : 'textarea', + id : 'hl_code', + rows : 22, + style : 'width:100%', + validate: CKEDITOR.dialog.validate.notEmpty( editor.lang.syntaxhighlight.sourceTextareaEmptyError ), + setup : function(f) { + if(f.code) { + this.setValue(f.code) + } + }, + commit : function(f) { + f.code=this.getValue() + } + } + ] + }, + { + id : 'advanced', + label : editor.lang.syntaxhighlight.advancedTab, + accessKey : 'A', + elements : [ + { + type : 'vbox', + children : [ + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.hideGutter+'' + }, + { + type : 'checkbox', + id : 'hide_gutter', + label : editor.lang.syntaxhighlight.hideGutterLbl, + setup : function(f) { + this.setValue(f.hideGutter) + }, + commit : function(f) { + f.hideGutter=this.getValue() + } + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.hideControls+'' + }, + { + type : 'checkbox', + id : 'hide_controls', + label : editor.lang.syntaxhighlight.hideControlsLbl, + setup : function(f) { + this.setValue(f.hideControls) + }, + commit : function(f) { + f.hideControls=this.getValue() + } + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.collapse+'' + }, + { + type : 'checkbox', + id : 'collapse', + label : editor.lang.syntaxhighlight.collapseLbl, + setup : function(f) { + this.setValue(f.collapse) + }, + commit : function(f) { + f.collapse=this.getValue() + } + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.showColumns+'' + }, + { + type : 'checkbox', + id : 'show_columns', + label : editor.lang.syntaxhighlight.showColumnsLbl, + setup : function(f) { + this.setValue(f.showColumns) + }, + commit : function(f) { + f.showColumns=this.getValue() + } + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.lineWrap+'' + }, + { + type : 'checkbox', + id : 'line_wrap', + label : editor.lang.syntaxhighlight.lineWrapLbl, + setup : function(f) { + this.setValue(f.noWrap) + }, + commit : function(f) { + f.noWrap=this.getValue() + } + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.lineCount+'' + }, + { + type : 'hbox', + widths : ['5%','95%'], + children : [ + { + type : 'checkbox', + id : 'lc_toggle', + label : '', + setup : function(f) { + this.setValue(f.firstLineChecked) + }, + commit : function(f) { + f.firstLineChecked=this.getValue() + } + }, + { + type : 'text', + id : 'default_lc', + style : 'width:15%;', + label : '', + setup : function(f) { + if(f.firstLine>1) { + this.setValue(f.firstLine) + } + }, + commit : function(f) { + if(this.getValue()&&this.getValue()!='') { + f.firstLine=this.getValue() + } + } + } + ] + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.highlight+'' + }, + { + type : 'hbox', + widths : ['5%','95%'], + children : [ + { + type : 'checkbox', + id : 'hl_toggle', + label : '', + setup : function(f) { + this.setValue(f.highlightChecked) + }, + commit : function(f) { + f.highlightChecked=this.getValue() + } + }, + { + type : 'text', + id : 'default_hl', + style : 'width:40%;', + label : '', + setup : function(f) { + if(f.highlight!=null) { + this.setValue(f.highlight) + } + }, + commit : function(f) { + if(this.getValue()&&this.getValue()!='') { + f.highlight=this.getValue() + } + } + } + ] + }, + { + type : 'hbox', + widths : ['5%','95%'], + children : [ + { + type : 'html', + html : '' + }, + { + type : 'html', + html : ''+editor.lang.syntaxhighlight.highlightLbl+'' + } + ] + } + ] + } + ] + } + ], + onShow : function() { + var i=this.getParentEditor(); + var h=i.getSelection(); + var g=h.getStartElement(); + var k=g&&g.getAscendant('pre',true); + var j=''; + var f=null; + if(k) { + code=a(k.getHtml()); + f=b(k.getAttribute('class')); + f.code=code + } else { + f=e() + } + this.setupContent(f) + }, + onOk : function() { + var h=this.getParentEditor(); + var g=h.getSelection(); + var f=g.getStartElement(); + var k=f&&f.getAscendant('pre',true); + var i=e(); + this.commitContent(i); + var j=d(i); + if(k) { + k.setAttribute('class',j); + k.setText(i.code) + } else { + var l=new CKEDITOR.dom.element('pre'); + l.setAttribute('class',j); + l.setText(i.code); + h.insertElement(l) + } + } + } +}) \ No newline at end of file diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/icons/syntaxhighlight.png b/addons/--Syntax_Highlighting/syntaxhighlight/icons/syntaxhighlight.png new file mode 100644 index 0000000..3c5c917 Binary files /dev/null and b/addons/--Syntax_Highlighting/syntaxhighlight/icons/syntaxhighlight.png differ diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/lang/de.js b/addons/--Syntax_Highlighting/syntaxhighlight/lang/de.js new file mode 100644 index 0000000..3c49565 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/lang/de.js @@ -0,0 +1,21 @@ +CKEDITOR.plugins.setLang( 'syntaxhighlight', 'de', { + title:'Einen Quelltextabschnitt einfügen oder aktualisieren', + contextTitle:'Quelltext bearbeiten', + sourceTab:'Quelltext', + langLbl:'Sprache auswählen', + sourceTextareaEmptyError:'Das Quelltext-Feld darf nicht leer sein.', + advancedTab:'Erweitert', + hideGutter:'Verstecke Seitenleiste', + hideGutterLbl:'Verstecke Seitenleiste und Zeilennummern.', + hideControls:'Verstecke Kontrollfelder', + hideControlsLbl:'Verstecke die Menüleiste über dem Quelltextblock.', + collapse:'Einklappen', + collapseLbl:'Klappe den Quelltextblock standartmäßig ein. (Kontrollfelder müssen aktiviert werden)', + showColumns:'Spalten anzeigen', + showColumnsLbl:'Zeige Spalten jeder Zeile in der Kopfzeile an.', + lineWrap:'Zeilenumbruch', + lineWrapLbl:'Deaktiviere den Zeilenumbruch.', + lineCount:'Standart Zeilenanzahl', + highlight:'Zeilen hervorheben', + highlightLbl:'Geben Sie kommasepariert die Zeilen ein, die sie hervorheben wollen, z.B. 3,10,15.' +}); diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/lang/en.js b/addons/--Syntax_Highlighting/syntaxhighlight/lang/en.js new file mode 100644 index 0000000..1f822b6 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/lang/en.js @@ -0,0 +1,21 @@ +CKEDITOR.plugins.setLang( 'syntaxhighlight', 'en', { + title:'Add or update a code snippet', + contextTitle:'Edit source code', + sourceTab:'Source code', + langLbl:'Select language', + sourceTextareaEmptyError:'Source code mustn\'t be empty.', + advancedTab:'Advanced', + hideGutter:'Hide gutter', + hideGutterLbl:'Hide gutter & line numbers.', + hideControls:'Hide controls', + hideControlsLbl:'Hide code controls at the top of the code block.', + collapse:'Collapse', + collapseLbl:'Collapse the code block by default. (controls need to be turned on)', + showColumns:'Show columns', + showColumnsLbl:'Show row columns in the first line.', + lineWrap:'Disable line wrapping', + lineWrapLbl:'Switch off line wrapping.', + lineCount:'Default line count', + highlight:'Highlight lines', + highlightLbl:'Enter a comma seperated lines of lines you want to highlight, eg 3,10,15.' +}); diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/lang/fr.js b/addons/--Syntax_Highlighting/syntaxhighlight/lang/fr.js new file mode 100644 index 0000000..4548ade --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/lang/fr.js @@ -0,0 +1,21 @@ +CKEDITOR.plugins.setLang('syntaxhighlight', 'fr', { + title:'Ajouter ou modifier un morceau de code source', + contextTitle:'Éditer le code source', + sourceTab:'Code source', + langLbl:'Sélectionnez le langage:', + sourceTextareaEmptyError:'Le code source ne doit pas être vide', + advancedTab:'Avancé', + hideGutter:'Cacher la marge', + hideGutterLbl:'Cacher la marge et les numéros de ligne.', + hideControls:'Cacher la barre d\'outils', + hideControlsLbl:'Cacher la barre d\'outils en haut du bloc de code.', + collapse:'Réduire le code', + collapseLbl:'Réduire le bloc de code par défaut. (la barre d\'outils doit être activée)', + showColumns:'Montrer les colonnes', + showColumnsLbl:'Montrer les colonnes sur la première ligne.', + lineWrap:'Désactiver le retour à la ligne automatique', + lineWrapLbl:'Désactiver le retour à la ligne automatique (ne fonctionne plus avec SyntaxHighlighter 3).', + lineCount:'Numéro de la première ligne (différent de 1)', + highlight:'Surligner les lignes', + highlightLbl:'Entrez les numéros des lignes que vous souhaitez surligner, séparés par des virgules. ex 3,10,15.' +}); diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/lang/ja.js b/addons/--Syntax_Highlighting/syntaxhighlight/lang/ja.js new file mode 100644 index 0000000..e567e3b --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/lang/ja.js @@ -0,0 +1,21 @@ +CKEDITOR.plugins.setLang( 'syntaxhighlight', 'ja', { + title:'コードスニペットの追加または更新', + contextTitle:'ソースコードの編集', + sourceTab:'ソースコード', + langLbl:'ソースコードの言語', + sourceTextareaEmptyError:'空のソースコードは登録できません。', + advancedTab:'詳細', + hideGutter:'行番号', + hideGutterLbl:'行番号を非表示にする', + hideControls:'codeタグ', + hideControlsLbl:'コードブロックの先頭に置かれたcodeタグを非表示にする', + collapse:'折りたたみ', + collapseLbl:'デフォルトでコードブロックを折りたたむ(コントロールをONにする必要があります)', + showColumns:'列', + showColumnsLbl:'最初の行に行カラムを表示する', + lineWrap:'改行', + lineWrapLbl:'行が長くても改行しない', + lineCount:'デフォルトの行数', + highlight:'行のハイライト表示', + highlightLbl:'ハイライトで表示する行をカンマ区切りで指定します(例: 3,10,15)' +}); diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/lang/uk.js b/addons/--Syntax_Highlighting/syntaxhighlight/lang/uk.js new file mode 100644 index 0000000..32147d5 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/lang/uk.js @@ -0,0 +1,21 @@ +CKEDITOR.plugins.setLang( 'syntaxhighlight', 'uk', { + title:'Додати або оновити фрагмент коду', + contextTitle:'Редагувати вихідний код', + sourceTab:'Вихідний код', + langLbl:'Оберіть мову', + sourceTextareaEmptyError:'Вихідний код не повинен бути порожнім.', + advancedTab:'Розширені', + hideGutter:'Сховати бічну панель', + hideGutterLbl:'Сховати бічну панель та номери рядків.', + hideControls:'Сховати елементи керування', + hideControlsLbl:'Сховати елементи управління кодом у верхній частині блоку коду.', + collapse:'Згорнути', + collapseLbl:'Згорнути блок коду за замовчуванням. (елементи управління повинні бути включені)', + showColumns:'Показати стовпці', + showColumnsLbl:'Показувати стовпці кожного рядка у заголовку.', + lineWrap:'Вимкнути перенесення рядків', + lineWrapLbl:'Відключити перенесення рядків.', + lineCount:'Кількість рядків за замовчуванням', + highlight:'Виділення рядків', + highlightLbl:'Введіть через кому лінії рядків, які ви хочете виділити, наприклад 3,10,15.' +}); diff --git a/addons/--Syntax_Highlighting/syntaxhighlight/plugin.js b/addons/--Syntax_Highlighting/syntaxhighlight/plugin.js new file mode 100644 index 0000000..82d4980 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlight/plugin.js @@ -0,0 +1,31 @@ +CKEDITOR.plugins.add( 'syntaxhighlight', { + requires : 'dialog', + lang : 'en,de,fr,ja,uk', // %REMOVE_LINE_CORE% + icons : 'syntaxhighlight', // %REMOVE_LINE_CORE% + init : function( editor ) { + editor.addCommand( 'syntaxhighlightDialog', new CKEDITOR.dialogCommand( 'syntaxhighlightDialog' ) ); + editor.ui.addButton && editor.ui.addButton( 'Syntaxhighlight', + { + label : editor.lang.syntaxhighlight.title, + command : 'syntaxhighlightDialog', + toolbar : 'insert,98' + } ); + + if ( editor.contextMenu ) { + editor.addMenuGroup( 'syntaxhighlightGroup' ); + editor.addMenuItem( 'syntaxhighlightItem', { + label: editor.lang.syntaxhighlight.contextTitle, + icon: this.path + 'icons/syntaxhighlight.png', + command: 'syntaxhighlightDialog', + group: 'syntaxhighlightGroup' + }); + editor.contextMenu.addListener( function( element ) { + if ( element.getAscendant( 'pre', true ) ) { + return { syntaxhighlightItem: CKEDITOR.TRISTATE_OFF }; + } + }); + } + + CKEDITOR.dialog.add( 'syntaxhighlightDialog', this.path + 'dialogs/syntaxhighlight.js' ); + } +}); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shAutoloader.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shAutoloader.js new file mode 100644 index 0000000..4e29bdd --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shAutoloader.js @@ -0,0 +1,17 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(2(){1 h=5;h.I=2(){2 n(c,a){4(1 d=0;d|<|≥|>=|≤|<=|\*|\+|-|\/|÷|\^)/g, + css: 'color2' }, + + { regex: /\b(?:and|as|div|mod|not|or|return(?!\s&)(ing)?|equals|(is(n't| not)? )?equal( to)?|does(n't| not) equal|(is(n't| not)? )?(greater|less) than( or equal( to)?)?|(comes|does(n't| not) come) (after|before)|is(n't| not)?( in)? (back|front) of|is(n't| not)? behind|is(n't| not)?( (in|contained by))?|does(n't| not) contain|contain(s)?|(start|begin|end)(s)? with|((but|end) )?(consider|ignor)ing|prop(erty)?|(a )?ref(erence)?( to)?|repeat (until|while|with)|((end|exit) )?repeat|((else|end) )?if|else|(end )?(script|tell|try)|(on )?error|(put )?into|(of )?(it|me)|its|my|with (timeout( of)?|transaction)|end (timeout|transaction))\b/g, + css: 'keyword' }, + + { regex: /\b\d+(st|nd|rd|th)\b/g, // ordinals + css: 'keyword' }, + + { regex: /\b(?:about|above|against|around|at|below|beneath|beside|between|by|(apart|aside) from|(instead|out) of|into|on(to)?|over|since|thr(ough|u)|under)\b/g, + css: 'color3' }, + + { regex: /\b(?:adding folder items to|after receiving|choose( ((remote )?application|color|folder|from list|URL))?|clipboard info|set the clipboard to|(the )?clipboard|entire contents|display(ing| (alert|dialog|mode))?|document( (edited|file|nib name))?|file( (name|type))?|(info )?for|giving up after|(name )?extension|quoted form|return(ed)?|second(?! item)(s)?|list (disks|folder)|text item(s| delimiters)?|(Unicode )?text|(disk )?item(s)?|((current|list) )?view|((container|key) )?window|with (data|icon( (caution|note|stop))?|parameter(s)?|prompt|properties|seed|title)|case|diacriticals|hyphens|numeric strings|punctuation|white space|folder creation|application(s( folder)?| (processes|scripts position|support))?|((desktop )?(pictures )?|(documents|downloads|favorites|home|keychain|library|movies|music|public|scripts|sites|system|users|utilities|workflows) )folder|desktop|Folder Action scripts|font(s| panel)?|help|internet plugins|modem scripts|(system )?preferences|printer descriptions|scripting (additions|components)|shared (documents|libraries)|startup (disk|items)|temporary items|trash|on server|in AppleTalk zone|((as|long|short) )?user name|user (ID|locale)|(with )?password|in (bundle( with identifier)?|directory)|(close|open for) access|read|write( permission)?|(g|s)et eof|using( delimiters)?|starting at|default (answer|button|color|country code|entr(y|ies)|identifiers|items|name|location|script editor)|hidden( answer)?|open(ed| (location|untitled))?|error (handling|reporting)|(do( shell)?|load|run|store) script|administrator privileges|altering line endings|get volume settings|(alert|boot|input|mount|output|set) volume|output muted|(fax|random )?number|round(ing)?|up|down|toward zero|to nearest|as taught in school|system (attribute|info)|((AppleScript( Studio)?|system) )?version|(home )?directory|(IPv4|primary Ethernet) address|CPU (type|speed)|physical memory|time (stamp|to GMT)|replacing|ASCII (character|number)|localized string|from table|offset|summarize|beep|delay|say|(empty|multiple) selections allowed|(of|preferred) type|invisibles|showing( package contents)?|editable URL|(File|FTP|News|Media|Web) [Ss]ervers|Telnet hosts|Directory services|Remote applications|waiting until completion|saving( (in|to))?|path (for|to( (((current|frontmost) )?application|resource))?)|POSIX (file|path)|(background|RGB) color|(OK|cancel) button name|cancel button|button(s)?|cubic ((centi)?met(re|er)s|yards|feet|inches)|square ((kilo)?met(re|er)s|miles|yards|feet)|(centi|kilo)?met(re|er)s|miles|yards|feet|inches|lit(re|er)s|gallons|quarts|(kilo)?grams|ounces|pounds|degrees (Celsius|Fahrenheit|Kelvin)|print( (dialog|settings))?|clos(e(able)?|ing)|(de)?miniaturized|miniaturizable|zoom(ed|able)|attribute run|action (method|property|title)|phone|email|((start|end)ing|home) page|((birth|creation|current|custom|modification) )?date|((((phonetic )?(first|last|middle))|computer|host|maiden|related) |nick)?name|aim|icq|jabber|msn|yahoo|address(es)?|save addressbook|should enable action|city|country( code)?|formatte(r|d address)|(palette )?label|state|street|zip|AIM [Hh]andle(s)?|my card|select(ion| all)?|unsaved|(alpha )?value|entr(y|ies)|group|(ICQ|Jabber|MSN) handle|person|people|company|department|icon image|job title|note|organization|suffix|vcard|url|copies|collating|pages (across|down)|request print time|target( printer)?|((GUI Scripting|Script menu) )?enabled|show Computer scripts|(de)?activated|awake from nib|became (key|main)|call method|of (class|object)|center|clicked toolbar item|closed|for document|exposed|(can )?hide|idle|keyboard (down|up)|event( (number|type))?|launch(ed)?|load (image|movie|nib|sound)|owner|log|mouse (down|dragged|entered|exited|moved|up)|move|column|localization|resource|script|register|drag (info|types)|resigned (active|key|main)|resiz(e(d)?|able)|right mouse (down|dragged|up)|scroll wheel|(at )?index|should (close|open( untitled)?|quit( after last window closed)?|zoom)|((proposed|screen) )?bounds|show(n)?|behind|in front of|size (mode|to fit)|update(d| toolbar item)?|was (hidden|miniaturized)|will (become active|close|finish launching|hide|miniaturize|move|open|quit|(resign )?active|((maximum|minimum|proposed) )?size|show|zoom)|bundle|data source|movie|pasteboard|sound|tool(bar| tip)|(color|open|save) panel|coordinate system|frontmost|main( (bundle|menu|window))?|((services|(excluded from )?windows) )?menu|((executable|frameworks|resource|scripts|shared (frameworks|support)) )?path|(selected item )?identifier|data|content(s| view)?|character(s)?|click count|(command|control|option|shift) key down|context|delta (x|y|z)|key( code)?|location|pressure|unmodified characters|types|(first )?responder|playing|(allowed|selectable) identifiers|allows customization|(auto saves )?configuration|visible|image( name)?|menu form representation|tag|user(-| )defaults|associated file name|(auto|needs) display|current field editor|floating|has (resize indicator|shadow)|hides when deactivated|level|minimized (image|title)|opaque|position|release when closed|sheet|title(d)?)\b/g, + css: 'color3' }, + + { regex: new RegExp(this.getKeywords(specials), 'gm'), css: 'color3' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, + { regex: new RegExp(this.getKeywords(ordinals), 'gm'), css: 'keyword' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['applescript']; + + SyntaxHighlighter.brushes.AppleScript = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushBash.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushBash.js new file mode 100644 index 0000000..8c29696 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushBash.js @@ -0,0 +1,59 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'if fi then elif else for do done until while break continue case function return in eq ne ge le'; + var commands = 'alias apropos awk basename bash bc bg builtin bzip2 cal cat cd cfdisk chgrp chmod chown chroot' + + 'cksum clear cmp comm command cp cron crontab csplit cut date dc dd ddrescue declare df ' + + 'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' + + 'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' + + 'free fsck ftp gawk getopts grep groups gzip hash head history hostname id ifconfig ' + + 'import install join kill less let ln local locate logname logout look lpc lpr lprint ' + + 'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' + + 'mv netstat nice nl nohup nslookup open op passwd paste pathchk ping popd pr printcap ' + + 'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' + + 'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' + + 'sleep sort source split ssh strace su sudo sum symlink sync tail tar tee test time ' + + 'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' + + 'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' + + 'vi watch wc whereis which who whoami Wget xargs yes' + ; + + this.regexList = [ + { regex: /^#!.*$/gm, css: 'preprocessor bold' }, + { regex: /\/[\w-\/]+/gm, css: 'plain' }, + { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(this.getKeywords(commands), 'gm'), css: 'functions' } // commands + ]; + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['bash', 'shell']; + + SyntaxHighlighter.brushes.Bash = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCSharp.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCSharp.js new file mode 100644 index 0000000..079214e --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCSharp.js @@ -0,0 +1,65 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'abstract as base bool break byte case catch char checked class const ' + + 'continue decimal default delegate do double else enum event explicit ' + + 'extern false finally fixed float for foreach get goto if implicit in int ' + + 'interface internal is lock long namespace new null object operator out ' + + 'override params private protected public readonly ref return sbyte sealed set ' + + 'short sizeof stackalloc static string struct switch this throw true try ' + + 'typeof uint ulong unchecked unsafe ushort using virtual void while'; + + function fixComments(match, regexInfo) + { + var css = (match[0].indexOf("///") == 0) + ? 'color1' + : 'comments' + ; + + return [new SyntaxHighlighter.Match(match[0], match.index, css)]; + } + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, func : fixComments }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: /@"(?:[^"]|"")*"/g, css: 'string' }, // @-quoted strings + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /^\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // c# keyword + { regex: /\bpartial(?=\s+(?:class|interface|struct)\b)/g, css: 'keyword' }, // contextual keyword: 'partial' + { regex: /\byield(?=\s+(?:return|break)\b)/g, css: 'keyword' } // contextual keyword: 'yield' + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['c#', 'c-sharp', 'csharp']; + + SyntaxHighlighter.brushes.CSharp = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); + diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushColdFusion.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushColdFusion.js new file mode 100644 index 0000000..627dbb9 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushColdFusion.js @@ -0,0 +1,100 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Jen + // http://www.jensbits.com/2009/05/14/coldfusion-brush-for-syntaxhighlighter-plus + + var funcs = 'Abs ACos AddSOAPRequestHeader AddSOAPResponseHeader AjaxLink AjaxOnLoad ArrayAppend ArrayAvg ArrayClear ArrayDeleteAt ' + + 'ArrayInsertAt ArrayIsDefined ArrayIsEmpty ArrayLen ArrayMax ArrayMin ArraySet ArraySort ArraySum ArraySwap ArrayToList ' + + 'Asc ASin Atn BinaryDecode BinaryEncode BitAnd BitMaskClear BitMaskRead BitMaskSet BitNot BitOr BitSHLN BitSHRN BitXor ' + + 'Ceiling CharsetDecode CharsetEncode Chr CJustify Compare CompareNoCase Cos CreateDate CreateDateTime CreateObject ' + + 'CreateODBCDate CreateODBCDateTime CreateODBCTime CreateTime CreateTimeSpan CreateUUID DateAdd DateCompare DateConvert ' + + 'DateDiff DateFormat DatePart Day DayOfWeek DayOfWeekAsString DayOfYear DaysInMonth DaysInYear DE DecimalFormat DecrementValue ' + + 'Decrypt DecryptBinary DeleteClientVariable DeserializeJSON DirectoryExists DollarFormat DotNetToCFType Duplicate Encrypt ' + + 'EncryptBinary Evaluate Exp ExpandPath FileClose FileCopy FileDelete FileExists FileIsEOF FileMove FileOpen FileRead ' + + 'FileReadBinary FileReadLine FileSetAccessMode FileSetAttribute FileSetLastModified FileWrite Find FindNoCase FindOneOf ' + + 'FirstDayOfMonth Fix FormatBaseN GenerateSecretKey GetAuthUser GetBaseTagData GetBaseTagList GetBaseTemplatePath ' + + 'GetClientVariablesList GetComponentMetaData GetContextRoot GetCurrentTemplatePath GetDirectoryFromPath GetEncoding ' + + 'GetException GetFileFromPath GetFileInfo GetFunctionList GetGatewayHelper GetHttpRequestData GetHttpTimeString ' + + 'GetK2ServerDocCount GetK2ServerDocCountLimit GetLocale GetLocaleDisplayName GetLocalHostIP GetMetaData GetMetricData ' + + 'GetPageContext GetPrinterInfo GetProfileSections GetProfileString GetReadableImageFormats GetSOAPRequest GetSOAPRequestHeader ' + + 'GetSOAPResponse GetSOAPResponseHeader GetTempDirectory GetTempFile GetTemplatePath GetTickCount GetTimeZoneInfo GetToken ' + + 'GetUserRoles GetWriteableImageFormats Hash Hour HTMLCodeFormat HTMLEditFormat IIf ImageAddBorder ImageBlur ImageClearRect ' + + 'ImageCopy ImageCrop ImageDrawArc ImageDrawBeveledRect ImageDrawCubicCurve ImageDrawLine ImageDrawLines ImageDrawOval ' + + 'ImageDrawPoint ImageDrawQuadraticCurve ImageDrawRect ImageDrawRoundRect ImageDrawText ImageFlip ImageGetBlob ImageGetBufferedImage ' + + 'ImageGetEXIFTag ImageGetHeight ImageGetIPTCTag ImageGetWidth ImageGrayscale ImageInfo ImageNegative ImageNew ImageOverlay ImagePaste ' + + 'ImageRead ImageReadBase64 ImageResize ImageRotate ImageRotateDrawingAxis ImageScaleToFit ImageSetAntialiasing ImageSetBackgroundColor ' + + 'ImageSetDrawingColor ImageSetDrawingStroke ImageSetDrawingTransparency ImageSharpen ImageShear ImageShearDrawingAxis ImageTranslate ' + + 'ImageTranslateDrawingAxis ImageWrite ImageWriteBase64 ImageXORDrawingMode IncrementValue InputBaseN Insert Int IsArray IsBinary ' + + 'IsBoolean IsCustomFunction IsDate IsDDX IsDebugMode IsDefined IsImage IsImageFile IsInstanceOf IsJSON IsLeapYear IsLocalHost ' + + 'IsNumeric IsNumericDate IsObject IsPDFFile IsPDFObject IsQuery IsSimpleValue IsSOAPRequest IsStruct IsUserInAnyRole IsUserInRole ' + + 'IsUserLoggedIn IsValid IsWDDX IsXML IsXmlAttribute IsXmlDoc IsXmlElem IsXmlNode IsXmlRoot JavaCast JSStringFormat LCase Left Len ' + + 'ListAppend ListChangeDelims ListContains ListContainsNoCase ListDeleteAt ListFind ListFindNoCase ListFirst ListGetAt ListInsertAt ' + + 'ListLast ListLen ListPrepend ListQualify ListRest ListSetAt ListSort ListToArray ListValueCount ListValueCountNoCase LJustify Log ' + + 'Log10 LSCurrencyFormat LSDateFormat LSEuroCurrencyFormat LSIsCurrency LSIsDate LSIsNumeric LSNumberFormat LSParseCurrency LSParseDateTime ' + + 'LSParseEuroCurrency LSParseNumber LSTimeFormat LTrim Max Mid Min Minute Month MonthAsString Now NumberFormat ParagraphFormat ParseDateTime ' + + 'Pi PrecisionEvaluate PreserveSingleQuotes Quarter QueryAddColumn QueryAddRow QueryConvertForGrid QueryNew QuerySetCell QuotedValueList Rand ' + + 'Randomize RandRange REFind REFindNoCase ReleaseComObject REMatch REMatchNoCase RemoveChars RepeatString Replace ReplaceList ReplaceNoCase ' + + 'REReplace REReplaceNoCase Reverse Right RJustify Round RTrim Second SendGatewayMessage SerializeJSON SetEncoding SetLocale SetProfileString ' + + 'SetVariable Sgn Sin Sleep SpanExcluding SpanIncluding Sqr StripCR StructAppend StructClear StructCopy StructCount StructDelete StructFind ' + + 'StructFindKey StructFindValue StructGet StructInsert StructIsEmpty StructKeyArray StructKeyExists StructKeyList StructKeyList StructNew ' + + 'StructSort StructUpdate Tan TimeFormat ToBase64 ToBinary ToScript ToString Trim UCase URLDecode URLEncodedFormat URLSessionFormat Val ' + + 'ValueList VerifyClient Week Wrap Wrap WriteOutput XmlChildPos XmlElemNew XmlFormat XmlGetNodeType XmlNew XmlParse XmlSearch XmlTransform ' + + 'XmlValidate Year YesNoFormat'; + + var keywords = 'cfabort cfajaximport cfajaxproxy cfapplet cfapplication cfargument cfassociate cfbreak cfcache cfcalendar ' + + 'cfcase cfcatch cfchart cfchartdata cfchartseries cfcol cfcollection cfcomponent cfcontent cfcookie cfdbinfo ' + + 'cfdefaultcase cfdirectory cfdiv cfdocument cfdocumentitem cfdocumentsection cfdump cfelse cfelseif cferror ' + + 'cfexchangecalendar cfexchangeconnection cfexchangecontact cfexchangefilter cfexchangemail cfexchangetask ' + + 'cfexecute cfexit cffeed cffile cfflush cfform cfformgroup cfformitem cfftp cffunction cfgrid cfgridcolumn ' + + 'cfgridrow cfgridupdate cfheader cfhtmlhead cfhttp cfhttpparam cfif cfimage cfimport cfinclude cfindex ' + + 'cfinput cfinsert cfinterface cfinvoke cfinvokeargument cflayout cflayoutarea cfldap cflocation cflock cflog ' + + 'cflogin cfloginuser cflogout cfloop cfmail cfmailparam cfmailpart cfmenu cfmenuitem cfmodule cfNTauthenticate ' + + 'cfobject cfobjectcache cfoutput cfparam cfpdf cfpdfform cfpdfformparam cfpdfparam cfpdfsubform cfpod cfpop ' + + 'cfpresentation cfpresentationslide cfpresenter cfprint cfprocessingdirective cfprocparam cfprocresult ' + + 'cfproperty cfquery cfqueryparam cfregistry cfreport cfreportparam cfrethrow cfreturn cfsavecontent cfschedule ' + + 'cfscript cfsearch cfselect cfset cfsetting cfsilent cfslider cfsprydataset cfstoredproc cfswitch cftable ' + + 'cftextarea cfthread cfthrow cftimer cftooltip cftrace cftransaction cftree cftreeitem cftry cfupdate cfwddx ' + + 'cfwindow cfxml cfzip cfzipparam'; + + var operators = 'all and any between cross in join like not null or outer some'; + + this.regexList = [ + { regex: new RegExp('--(.*)$', 'gm'), css: 'comments' }, // one line and multiline comments + { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // single quoted strings + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // functions + { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword + ]; + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['coldfusion','cf']; + + SyntaxHighlighter.brushes.ColdFusion = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCpp.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCpp.js new file mode 100644 index 0000000..9f70d3a --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCpp.js @@ -0,0 +1,97 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Copyright 2006 Shin, YoungJin + + var datatypes = 'ATOM BOOL BOOLEAN BYTE CHAR COLORREF DWORD DWORDLONG DWORD_PTR ' + + 'DWORD32 DWORD64 FLOAT HACCEL HALF_PTR HANDLE HBITMAP HBRUSH ' + + 'HCOLORSPACE HCONV HCONVLIST HCURSOR HDC HDDEDATA HDESK HDROP HDWP ' + + 'HENHMETAFILE HFILE HFONT HGDIOBJ HGLOBAL HHOOK HICON HINSTANCE HKEY ' + + 'HKL HLOCAL HMENU HMETAFILE HMODULE HMONITOR HPALETTE HPEN HRESULT ' + + 'HRGN HRSRC HSZ HWINSTA HWND INT INT_PTR INT32 INT64 LANGID LCID LCTYPE ' + + 'LGRPID LONG LONGLONG LONG_PTR LONG32 LONG64 LPARAM LPBOOL LPBYTE LPCOLORREF ' + + 'LPCSTR LPCTSTR LPCVOID LPCWSTR LPDWORD LPHANDLE LPINT LPLONG LPSTR LPTSTR ' + + 'LPVOID LPWORD LPWSTR LRESULT PBOOL PBOOLEAN PBYTE PCHAR PCSTR PCTSTR PCWSTR ' + + 'PDWORDLONG PDWORD_PTR PDWORD32 PDWORD64 PFLOAT PHALF_PTR PHANDLE PHKEY PINT ' + + 'PINT_PTR PINT32 PINT64 PLCID PLONG PLONGLONG PLONG_PTR PLONG32 PLONG64 POINTER_32 ' + + 'POINTER_64 PSHORT PSIZE_T PSSIZE_T PSTR PTBYTE PTCHAR PTSTR PUCHAR PUHALF_PTR ' + + 'PUINT PUINT_PTR PUINT32 PUINT64 PULONG PULONGLONG PULONG_PTR PULONG32 PULONG64 ' + + 'PUSHORT PVOID PWCHAR PWORD PWSTR SC_HANDLE SC_LOCK SERVICE_STATUS_HANDLE SHORT ' + + 'SIZE_T SSIZE_T TBYTE TCHAR UCHAR UHALF_PTR UINT UINT_PTR UINT32 UINT64 ULONG ' + + 'ULONGLONG ULONG_PTR ULONG32 ULONG64 USHORT USN VOID WCHAR WORD WPARAM WPARAM WPARAM ' + + 'char bool short int __int32 __int64 __int8 __int16 long float double __wchar_t ' + + 'clock_t _complex _dev_t _diskfree_t div_t ldiv_t _exception _EXCEPTION_POINTERS ' + + 'FILE _finddata_t _finddatai64_t _wfinddata_t _wfinddatai64_t __finddata64_t ' + + '__wfinddata64_t _FPIEEE_RECORD fpos_t _HEAPINFO _HFILE lconv intptr_t ' + + 'jmp_buf mbstate_t _off_t _onexit_t _PNH ptrdiff_t _purecall_handler ' + + 'sig_atomic_t size_t _stat __stat64 _stati64 terminate_function ' + + 'time_t __time64_t _timeb __timeb64 tm uintptr_t _utimbuf ' + + 'va_list wchar_t wctrans_t wctype_t wint_t signed'; + + var keywords = 'break case catch class const __finally __exception __try ' + + 'const_cast continue private public protected __declspec ' + + 'default delete deprecated dllexport dllimport do dynamic_cast ' + + 'else enum explicit extern if for friend goto inline ' + + 'mutable naked namespace new noinline noreturn nothrow ' + + 'register reinterpret_cast return selectany ' + + 'sizeof static static_cast struct switch template this ' + + 'thread throw true false try typedef typeid typename union ' + + 'using uuid virtual void volatile whcar_t while'; + + var functions = 'assert isalnum isalpha iscntrl isdigit isgraph islower isprint' + + 'ispunct isspace isupper isxdigit tolower toupper errno localeconv ' + + 'setlocale acos asin atan atan2 ceil cos cosh exp fabs floor fmod ' + + 'frexp ldexp log log10 modf pow sin sinh sqrt tan tanh jmp_buf ' + + 'longjmp setjmp raise signal sig_atomic_t va_arg va_end va_start ' + + 'clearerr fclose feof ferror fflush fgetc fgetpos fgets fopen ' + + 'fprintf fputc fputs fread freopen fscanf fseek fsetpos ftell ' + + 'fwrite getc getchar gets perror printf putc putchar puts remove ' + + 'rename rewind scanf setbuf setvbuf sprintf sscanf tmpfile tmpnam ' + + 'ungetc vfprintf vprintf vsprintf abort abs atexit atof atoi atol ' + + 'bsearch calloc div exit free getenv labs ldiv malloc mblen mbstowcs ' + + 'mbtowc qsort rand realloc srand strtod strtol strtoul system ' + + 'wcstombs wctomb memchr memcmp memcpy memmove memset strcat strchr ' + + 'strcmp strcoll strcpy strcspn strerror strlen strncat strncmp ' + + 'strncpy strpbrk strrchr strspn strstr strtok strxfrm asctime ' + + 'clock ctime difftime gmtime localtime mktime strftime time'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /^ *#.*/gm, css: 'preprocessor' }, + { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'color1 bold' }, + { regex: new RegExp(this.getKeywords(functions), 'gm'), css: 'functions bold' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword bold' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['cpp', 'c']; + + SyntaxHighlighter.brushes.Cpp = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCss.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCss.js new file mode 100644 index 0000000..4297a9a --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushCss.js @@ -0,0 +1,91 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + function getKeywordsCSS(str) + { + return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b'; + }; + + function getValuesCSS(str) + { + return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b'; + }; + + var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + + 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + + 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + + 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + + 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + + 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + + 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + + 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + + 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + + 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + + 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + + 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + + 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + + 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index'; + + var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+ + 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+ + 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double '+ + 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+ + 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+ + 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+ + 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+ + 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+ + 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+ + 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+ + 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+ + 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+ + 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+ + 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow'; + + var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors + { regex: /(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)/g, css: 'value' }, // sizes + { regex: /!important/g, css: 'color3' }, // !important + { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values + { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts + ]; + + this.forHtmlScript({ + left: /(<|<)\s*style.*?(>|>)/gi, + right: /(<|<)\/\s*style\s*(>|>)/gi + }); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['css']; + + SyntaxHighlighter.brushes.CSS = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushDelphi.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushDelphi.js new file mode 100644 index 0000000..e1060d4 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushDelphi.js @@ -0,0 +1,55 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'abs addr and ansichar ansistring array as asm begin boolean byte cardinal ' + + 'case char class comp const constructor currency destructor div do double ' + + 'downto else end except exports extended false file finalization finally ' + + 'for function goto if implementation in inherited int64 initialization ' + + 'integer interface is label library longint longword mod nil not object ' + + 'of on or packed pansichar pansistring pchar pcurrency pdatetime pextended ' + + 'pint64 pointer private procedure program property pshortstring pstring ' + + 'pvariant pwidechar pwidestring protected public published raise real real48 ' + + 'record repeat set shl shortint shortstring shr single smallint string then ' + + 'threadvar to true try type unit until uses val var varirnt while widechar ' + + 'widestring with word write writeln xor'; + + this.regexList = [ + { regex: /\(\*[\s\S]*?\*\)/gm, css: 'comments' }, // multiline comments (* *) + { regex: /{(?!\$)[\s\S]*?}/gm, css: 'comments' }, // multiline comments { } + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /\{\$[a-zA-Z]+ .+\}/g, css: 'color1' }, // compiler Directives and Region tags + { regex: /\b[\d\.]+\b/g, css: 'value' }, // numbers 12345 + { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // numbers $F5D3 + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['delphi', 'pascal', 'pas']; + + SyntaxHighlighter.brushes.Delphi = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushDiff.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushDiff.js new file mode 100644 index 0000000..e9b14fc --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushDiff.js @@ -0,0 +1,41 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + this.regexList = [ + { regex: /^\+\+\+.*$/gm, css: 'color2' }, + { regex: /^\-\-\-.*$/gm, css: 'color2' }, + { regex: /^\s.*$/gm, css: 'color1' }, + { regex: /^@@.*@@$/gm, css: 'variable' }, + { regex: /^\+[^\+]{1}.*$/gm, css: 'string' }, + { regex: /^\-[^\-]{1}.*$/gm, css: 'comments' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['diff', 'patch']; + + SyntaxHighlighter.brushes.Diff = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushErlang.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushErlang.js new file mode 100644 index 0000000..6ba7d9d --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushErlang.js @@ -0,0 +1,52 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Jean-Lou Dupont + // http://jldupont.blogspot.com/2009/06/erlang-syntax-highlighter.html + + // According to: http://erlang.org/doc/reference_manual/introduction.html#1.5 + var keywords = 'after and andalso band begin bnot bor bsl bsr bxor '+ + 'case catch cond div end fun if let not of or orelse '+ + 'query receive rem try when xor'+ + // additional + ' module export import define'; + + this.regexList = [ + { regex: new RegExp("[A-Z][A-Za-z0-9_]+", 'g'), css: 'constants' }, + { regex: new RegExp("\\%.+", 'gm'), css: 'comments' }, + { regex: new RegExp("\\?[A-Za-z0-9_]+", 'g'), css: 'preprocessor' }, + { regex: new RegExp("[a-z0-9_]+:[a-z0-9_]+", 'g'), css: 'functions' }, + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['erl', 'erlang']; + + SyntaxHighlighter.brushes.Erland = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushGroovy.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushGroovy.js new file mode 100644 index 0000000..6ec5c18 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushGroovy.js @@ -0,0 +1,67 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Andres Almiray + // http://jroller.com/aalmiray/entry/nice_source_code_syntax_highlighter + + var keywords = 'as assert break case catch class continue def default do else extends finally ' + + 'if in implements import instanceof interface new package property return switch ' + + 'throw throws try while public protected private static'; + var types = 'void boolean byte char short int long float double'; + var constants = 'null'; + var methods = 'allProperties count get size '+ + 'collect each eachProperty eachPropertyName eachWithIndex find findAll ' + + 'findIndexOf grep inject max min reverseEach sort ' + + 'asImmutable asSynchronized flatten intersect join pop reverse subMap toList ' + + 'padRight padLeft contains eachMatch toCharacter toLong toUrl tokenize ' + + 'eachFile eachFileRecurse eachB yte eachLine readBytes readLine getText ' + + 'splitEachLine withReader append encodeBase64 decodeBase64 filterLine ' + + 'transformChar transformLine withOutputStream withPrintWriter withStream ' + + 'withStreams withWriter withWriterAppend write writeLine '+ + 'dump inspect invokeMethod print println step times upto use waitForOrKill '+ + 'getText'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /""".*"""/g, css: 'string' }, // GStrings + { regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'), css: 'value' }, // numbers + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // goovy keyword + { regex: new RegExp(this.getKeywords(types), 'gm'), css: 'color1' }, // goovy/java type + { regex: new RegExp(this.getKeywords(constants), 'gm'), css: 'constants' }, // constants + { regex: new RegExp(this.getKeywords(methods), 'gm'), css: 'functions' } // methods + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['groovy']; + + SyntaxHighlighter.brushes.Groovy = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushIni.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushIni.js new file mode 100644 index 0000000..fbadb39 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushIni.js @@ -0,0 +1,67 @@ +/** + * Ini brush for Code Syntax Highlighter http://alexgorbatchev.com/SyntaxHighlighter + * by Boris Guéry, http://borisguery.com + * + * DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + * Version 2, December 2004 + * + * Copyright (C) 2004 Sam Hocevar «sam@hocevar.net» + * + * Everyone is permitted to copy and distribute verbatim or modified + * copies of this license document, and changing it is allowed as long + * as the name is changed. + * + * DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + * + * 0. You just DO WHAT THE FUCK YOU WANT TO. + * + */ + +// CommonJS +typeof (require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter + : null; + +function Brush() { + + var keywords = 'false true on off'; + + this.regexList = [ + { + regex : SyntaxHighlighter.regexLib.doubleQuotedString, + css : 'string' + }, // double quoted strings + { + regex : SyntaxHighlighter.regexLib.singleQuotedString, + css : 'string' + }, // single quoted strings + { + regex : /\b[-+]?[0-9]*\.?[0-9]+\b/g, + css : 'number' + }, // numbers (int or float) + { + regex : /;.*/g, + css : 'comments' + }, + { + regex : /\[[a-z0-9:\-\s]+\]/gi, + css : 'color3' + }, + { + regex: /\w+(\[\])*(?=\s*=)/g, + css: 'variable' + }, + { + regex: new RegExp(this.getKeywords(keywords), 'gmi'), + css: 'keyword' + } + ]; +}; + +Brush.prototype = new SyntaxHighlighter.Highlighter(); +Brush.aliases = ['ini']; + +SyntaxHighlighter.brushes.Ini = Brush; + +// CommonJS +typeof (exports) != 'undefined' ? exports.Brush = Brush : null; diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJScript.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJScript.js new file mode 100644 index 0000000..ff98dab --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJScript.js @@ -0,0 +1,52 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'break case catch continue ' + + 'default delete do else false ' + + 'for function if in instanceof ' + + 'new null return super switch ' + + 'this throw true try typeof var while with' + ; + + var r = SyntaxHighlighter.regexLib; + + this.regexList = [ + { regex: r.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings + { regex: r.multiLineSingleQuotedString, css: 'string' }, // single quoted strings + { regex: r.singleLineCComments, css: 'comments' }, // one line comments + { regex: r.multiLineCComments, css: 'comments' }, // multiline comments + { regex: /\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keywords + ]; + + this.forHtmlScript(r.scriptScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['js', 'jscript', 'javascript']; + + SyntaxHighlighter.brushes.JScript = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJava.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJava.js new file mode 100644 index 0000000..d692fd6 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJava.js @@ -0,0 +1,57 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'abstract assert boolean break byte case catch char class const ' + + 'continue default do double else enum extends ' + + 'false final finally float for goto if implements import ' + + 'instanceof int interface long native new null ' + + 'package private protected public return ' + + 'short static strictfp super switch synchronized this throw throws true ' + + 'transient try void volatile while'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: /\/\*([^\*][\s\S]*)?\*\//gm, css: 'comments' }, // multiline comments + { regex: /\/\*(?!\*\/)\*[\s\S]*?\*\//gm, css: 'preprocessor' }, // documentation comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers + { regex: /(?!\@interface\b)\@[\$\w]+\b/g, css: 'color1' }, // annotation @anno + { regex: /\@interface\b/g, css: 'color2' }, // @interface keyword + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // java keyword + ]; + + this.forHtmlScript({ + left : /(<|<)%[@!=]?/g, + right : /%(>|>)/g + }); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['java']; + + SyntaxHighlighter.brushes.Java = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJavaFX.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJavaFX.js new file mode 100644 index 0000000..1a150a6 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushJavaFX.js @@ -0,0 +1,58 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Patrick Webster + // http://patrickwebster.blogspot.com/2009/04/javafx-brush-for-syntaxhighlighter.html + var datatypes = 'Boolean Byte Character Double Duration ' + + 'Float Integer Long Number Short String Void' + ; + + var keywords = 'abstract after and as assert at before bind bound break catch class ' + + 'continue def delete else exclusive extends false finally first for from ' + + 'function if import in indexof init insert instanceof into inverse last ' + + 'lazy mixin mod nativearray new not null on or override package postinit ' + + 'protected public public-init public-read replace return reverse sizeof ' + + 'step super then this throw true try tween typeof var where while with ' + + 'attribute let private readonly static trigger' + ; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, + { regex: /(-?\.?)(\b(\d*\.?\d+|\d+\.?\d*)(e[+-]?\d+)?|0x[a-f\d]+)\b\.?/gi, css: 'color2' }, // numbers + { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'variable' }, // datatypes + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } + ]; + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['jfx', 'javafx']; + + SyntaxHighlighter.brushes.JavaFX = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPerl.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPerl.js new file mode 100644 index 0000000..d94a2e0 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPerl.js @@ -0,0 +1,72 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by David Simmons-Duffin and Marty Kube + + var funcs = + 'abs accept alarm atan2 bind binmode chdir chmod chomp chop chown chr ' + + 'chroot close closedir connect cos crypt defined delete each endgrent ' + + 'endhostent endnetent endprotoent endpwent endservent eof exec exists ' + + 'exp fcntl fileno flock fork format formline getc getgrent getgrgid ' + + 'getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr ' + + 'getnetbyname getnetent getpeername getpgrp getppid getpriority ' + + 'getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid ' + + 'getservbyname getservbyport getservent getsockname getsockopt glob ' + + 'gmtime grep hex index int ioctl join keys kill lc lcfirst length link ' + + 'listen localtime lock log lstat map mkdir msgctl msgget msgrcv msgsnd ' + + 'oct open opendir ord pack pipe pop pos print printf prototype push ' + + 'quotemeta rand read readdir readline readlink readpipe recv rename ' + + 'reset reverse rewinddir rindex rmdir scalar seek seekdir select semctl ' + + 'semget semop send setgrent sethostent setnetent setpgrp setpriority ' + + 'setprotoent setpwent setservent setsockopt shift shmctl shmget shmread ' + + 'shmwrite shutdown sin sleep socket socketpair sort splice split sprintf ' + + 'sqrt srand stat study substr symlink syscall sysopen sysread sysseek ' + + 'system syswrite tell telldir time times tr truncate uc ucfirst umask ' + + 'undef unlink unpack unshift utime values vec wait waitpid warn write'; + + var keywords = + 'bless caller continue dbmclose dbmopen die do dump else elsif eval exit ' + + 'for foreach goto if import last local my next no our package redo ref ' + + 'require return sub tie tied unless untie until use wantarray while'; + + this.regexList = [ + { regex: new RegExp('#[^!].*$', 'gm'), css: 'comments' }, + { regex: new RegExp('^\\s*#!.*$', 'gm'), css: 'preprocessor' }, // shebang + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, + { regex: new RegExp('(\\$|@|%)\\w+', 'g'), css: 'variable' }, + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['perl', 'Perl', 'pl']; + + SyntaxHighlighter.brushes.Perl = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPhp.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPhp.js new file mode 100644 index 0000000..95e6e43 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPhp.js @@ -0,0 +1,88 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var funcs = 'abs acos acosh addcslashes addslashes ' + + 'array_change_key_case array_chunk array_combine array_count_values array_diff '+ + 'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill '+ + 'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key '+ + 'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map '+ + 'array_merge array_merge_recursive array_multisort array_pad array_pop array_product '+ + 'array_push array_rand array_reduce array_reverse array_search array_shift '+ + 'array_slice array_splice array_sum array_udiff array_udiff_assoc '+ + 'array_udiff_uassoc array_uintersect array_uintersect_assoc '+ + 'array_uintersect_uassoc array_unique array_unshift array_values array_walk '+ + 'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert '+ + 'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress '+ + 'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir '+ + 'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists '+ + 'closedir closelog copy cos cosh count count_chars date decbin dechex decoct '+ + 'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log '+ + 'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded '+ + 'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents '+ + 'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype '+ + 'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf '+ + 'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname '+ + 'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt '+ + 'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext '+ + 'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set '+ + 'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double '+ + 'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long '+ + 'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault '+ + 'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br '+ + 'parse_ini_file parse_str parse_url passthru pathinfo print readlink realpath rewind rewinddir rmdir '+ + 'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split '+ + 'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes '+ + 'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk '+ + 'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime '+ + 'strtoupper strtr strval substr substr_compare'; + + var keywords = 'abstract and array as break case catch cfunction class clone const continue declare default die do ' + + 'else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach ' + + 'function include include_once global goto if implements interface instanceof namespace new ' + + 'old_function or private protected public return require require_once static switch ' + + 'throw try use var while xor '; + + var constants = '__FILE__ __LINE__ __METHOD__ __FUNCTION__ __CLASS__'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\$\w+/g, css: 'variable' }, // variables + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // common functions + { regex: new RegExp(this.getKeywords(constants), 'gmi'), css: 'constants' }, // constants + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keyword + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['php']; + + SyntaxHighlighter.brushes.Php = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPlain.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPlain.js new file mode 100644 index 0000000..9f7d9e9 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPlain.js @@ -0,0 +1,33 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['text', 'plain']; + + SyntaxHighlighter.brushes.Plain = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPowerShell.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPowerShell.js new file mode 100644 index 0000000..0be1752 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPowerShell.js @@ -0,0 +1,74 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributes by B.v.Zanten, Getronics + // http://confluence.atlassian.com/display/CONFEXT/New+Code+Macro + + var keywords = 'Add-Content Add-History Add-Member Add-PSSnapin Clear(-Content)? Clear-Item ' + + 'Clear-ItemProperty Clear-Variable Compare-Object ConvertFrom-SecureString Convert-Path ' + + 'ConvertTo-Html ConvertTo-SecureString Copy(-Item)? Copy-ItemProperty Export-Alias ' + + 'Export-Clixml Export-Console Export-Csv ForEach(-Object)? Format-Custom Format-List ' + + 'Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command ' + + 'Get-Content Get-Credential Get-Culture Get-Date Get-EventLog Get-ExecutionPolicy ' + + 'Get-Help Get-History Get-Host Get-Item Get-ItemProperty Get-Location Get-Member ' + + 'Get-PfxCertificate Get-Process Get-PSDrive Get-PSProvider Get-PSSnapin Get-Service ' + + 'Get-TraceSource Get-UICulture Get-Unique Get-Variable Get-WmiObject Group-Object ' + + 'Import-Alias Import-Clixml Import-Csv Invoke-Expression Invoke-History Invoke-Item ' + + 'Join-Path Measure-Command Measure-Object Move(-Item)? Move-ItemProperty New-Alias ' + + 'New-Item New-ItemProperty New-Object New-PSDrive New-Service New-TimeSpan ' + + 'New-Variable Out-Default Out-File Out-Host Out-Null Out-Printer Out-String Pop-Location ' + + 'Push-Location Read-Host Remove-Item Remove-ItemProperty Remove-PSDrive Remove-PSSnapin ' + + 'Remove-Variable Rename-Item Rename-ItemProperty Resolve-Path Restart-Service Resume-Service ' + + 'Select-Object Select-String Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content ' + + 'Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-Location Set-PSDebug ' + + 'Set-Service Set-TraceSource Set(-Variable)? Sort-Object Split-Path Start-Service ' + + 'Start-Sleep Start-Transcript Stop-Process Stop-Service Stop-Transcript Suspend-Service ' + + 'Tee-Object Test-Path Trace-Command Update-FormatData Update-TypeData Where(-Object)? ' + + 'Write-Debug Write-Error Write(-Host)? Write-Output Write-Progress Write-Verbose Write-Warning'; + var alias = 'ac asnp clc cli clp clv cpi cpp cvpa diff epal epcsv fc fl ' + + 'ft fw gal gc gci gcm gdr ghy gi gl gm gp gps group gsv ' + + 'gsnp gu gv gwmi iex ihy ii ipal ipcsv mi mp nal ndr ni nv oh rdr ' + + 'ri rni rnp rp rsnp rv rvpa sal sasv sc select si sl sleep sort sp ' + + 'spps spsv sv tee cat cd cp h history kill lp ls ' + + 'mount mv popd ps pushd pwd r rm rmdir echo cls chdir del dir ' + + 'erase rd ren type % \\?'; + + this.regexList = [ + { regex: /#.*$/gm, css: 'comments' }, // one line comments + { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // variables $Computer1 + { regex: /\-[a-zA-Z]+\b/g, css: 'keyword' }, // Operators -not -and -eq + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' }, + { regex: new RegExp(this.getKeywords(alias), 'gmi'), css: 'keyword' } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['powershell', 'ps']; + + SyntaxHighlighter.brushes.PowerShell = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPython.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPython.js new file mode 100644 index 0000000..ce77462 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushPython.js @@ -0,0 +1,64 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Gheorghe Milas and Ahmad Sherif + + var keywords = 'and assert break class continue def del elif else ' + + 'except exec finally for from global if import in is ' + + 'lambda not or pass print raise return try yield while'; + + var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' + + 'chr classmethod cmp coerce compile complex delattr dict dir ' + + 'divmod enumerate eval execfile file filter float format frozenset ' + + 'getattr globals hasattr hash help hex id input int intern ' + + 'isinstance issubclass iter len list locals long map max min next ' + + 'object oct open ord pow print property range raw_input reduce ' + + 'reload repr reversed round set setattr slice sorted staticmethod ' + + 'str sum super tuple type type unichr unicode vars xrange zip'; + + var special = 'None True False self cls class_'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, + { regex: /^\s*@\w+/gm, css: 'decorator' }, + { regex: /(['\"]{3})([^\1])*?\1/gm, css: 'comments' }, + { regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, css: 'string' }, + { regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, css: 'string' }, + { regex: /\+|\-|\*|\/|\%|=|==/gm, css: 'keyword' }, + { regex: /\b\d+\.?\w*/g, css: 'value' }, + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, + { regex: new RegExp(this.getKeywords(special), 'gm'), css: 'color1' } + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['py', 'python']; + + SyntaxHighlighter.brushes.Python = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushRuby.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushRuby.js new file mode 100644 index 0000000..ff82130 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushRuby.js @@ -0,0 +1,55 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Erik Peterson. + + var keywords = 'alias and BEGIN begin break case class def define_method defined do each else elsif ' + + 'END end ensure false for if in module new next nil not or raise redo rescue retry return ' + + 'self super then throw true undef unless until when while yield'; + + var builtins = 'Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload ' + + 'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ' + + 'ThreadGroup Thread Time TrueClass'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\b[A-Z0-9_]+\b/g, css: 'constants' }, // constants + { regex: /:[a-z][A-Za-z0-9_]*/g, css: 'color2' }, // symbols + { regex: /(\$|@@|@)\w+/g, css: 'variable bold' }, // $global, @instance, and @@class variables + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(this.getKeywords(builtins), 'gm'), css: 'color1' } // builtins + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['ruby', 'rails', 'ror', 'rb']; + + SyntaxHighlighter.brushes.Ruby = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushSass.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushSass.js new file mode 100644 index 0000000..aa04da0 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushSass.js @@ -0,0 +1,94 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + function getKeywordsCSS(str) + { + return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b'; + }; + + function getValuesCSS(str) + { + return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b'; + }; + + var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + + 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + + 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + + 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + + 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + + 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + + 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + + 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + + 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + + 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + + 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + + 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + + 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + + 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index'; + + var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+ + 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+ + 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero digits disc dotted double '+ + 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+ + 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+ + 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+ + 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+ + 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+ + 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+ + 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+ + 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+ + 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+ + 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+ + 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow'; + + var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif'; + + var statements = '!important !default'; + var preprocessor = '@import @extend @debug @warn @if @for @while @mixin @include'; + + var r = SyntaxHighlighter.regexLib; + + this.regexList = [ + { regex: r.multiLineCComments, css: 'comments' }, // multiline comments + { regex: r.singleLineCComments, css: 'comments' }, // singleline comments + { regex: r.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: r.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors + { regex: /\b(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)\b/g, css: 'value' }, // sizes + { regex: /\$\w+/g, css: 'variable' }, // variables + { regex: new RegExp(this.getKeywords(statements), 'g'), css: 'color3' }, // statements + { regex: new RegExp(this.getKeywords(preprocessor), 'g'), css: 'preprocessor' }, // preprocessor + { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values + { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['sass', 'scss']; + + SyntaxHighlighter.brushes.Sass = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushScala.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushScala.js new file mode 100644 index 0000000..4b0b6f0 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushScala.js @@ -0,0 +1,51 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + // Contributed by Yegor Jbanov and David Bernard. + + var keywords = 'val sealed case def true trait implicit forSome import match object null finally super ' + + 'override try lazy for var catch throw type extends class while with new final yield abstract ' + + 'else do if return protected private this package false'; + + var keyops = '[_:=><%#@]+'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // multi-line strings + { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double-quoted string + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings + { regex: /0x[a-f0-9]+|\d+(\.\d+)?/gi, css: 'value' }, // numbers + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords + { regex: new RegExp(keyops, 'gm'), css: 'keyword' } // scala keyword + ]; + } + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['scala']; + + SyntaxHighlighter.brushes.Scala = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushSql.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushSql.js new file mode 100644 index 0000000..5c2cd88 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushSql.js @@ -0,0 +1,66 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var funcs = 'abs avg case cast coalesce convert count current_timestamp ' + + 'current_user day isnull left lower month nullif replace right ' + + 'session_user space substring sum system_user upper user year'; + + var keywords = 'absolute action add after alter as asc at authorization begin bigint ' + + 'binary bit by cascade char character check checkpoint close collate ' + + 'column commit committed connect connection constraint contains continue ' + + 'create cube current current_date current_time cursor database date ' + + 'deallocate dec decimal declare default delete desc distinct double drop ' + + 'dynamic else end end-exec escape except exec execute false fetch first ' + + 'float for force foreign forward free from full function global goto grant ' + + 'group grouping having hour ignore index inner insensitive insert instead ' + + 'int integer intersect into is isolation key last level load local max min ' + + 'minute modify move name national nchar next no numeric of off on only ' + + 'open option order out output partial password precision prepare primary ' + + 'prior privileges procedure public read real references relative repeatable ' + + 'restrict return returns revoke rollback rollup rows rule schema scroll ' + + 'second section select sequence serializable set size smallint static ' + + 'statistics table temp temporary then time timestamp to top transaction ' + + 'translation trigger true truncate uncommitted union unique update values ' + + 'varchar varying view when where with work'; + + var operators = 'all and any between cross in join like not null or outer some'; + + this.regexList = [ + { regex: /--(.*)$/gm, css: 'comments' }, // one line and multiline comments + { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // single quoted strings + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'color2' }, // functions + { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such + { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['sql']; + + SyntaxHighlighter.brushes.Sql = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); + diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushVb.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushVb.js new file mode 100644 index 0000000..be845dc --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushVb.js @@ -0,0 +1,56 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + var keywords = 'AddHandler AddressOf AndAlso Alias And Ansi As Assembly Auto ' + + 'Boolean ByRef Byte ByVal Call Case Catch CBool CByte CChar CDate ' + + 'CDec CDbl Char CInt Class CLng CObj Const CShort CSng CStr CType ' + + 'Date Decimal Declare Default Delegate Dim DirectCast Do Double Each ' + + 'Else ElseIf End Enum Erase Error Event Exit False Finally For Friend ' + + 'Function Get GetType GoSub GoTo Handles If Implements Imports In ' + + 'Inherits Integer Interface Is Let Lib Like Long Loop Me Mod Module ' + + 'MustInherit MustOverride MyBase MyClass Namespace New Next Not Nothing ' + + 'NotInheritable NotOverridable Object On Option Optional Or OrElse ' + + 'Overloads Overridable Overrides ParamArray Preserve Private Property ' + + 'Protected Public RaiseEvent ReadOnly ReDim REM RemoveHandler Resume ' + + 'Return Select Set Shadows Shared Short Single Static Step Stop String ' + + 'Structure Sub SyncLock Then Throw To True Try TypeOf Unicode Until ' + + 'Variant When While With WithEvents WriteOnly Xor'; + + this.regexList = [ + { regex: /'.*$/gm, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings + { regex: /^\s*#.*$/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // vb keyword + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['vb', 'vbnet']; + + SyntaxHighlighter.brushes.Vb = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushXml.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushXml.js new file mode 100644 index 0000000..69d9fd0 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shBrushXml.js @@ -0,0 +1,69 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +;(function() +{ + // CommonJS + typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; + + function Brush() + { + function process(match, regexInfo) + { + var constructor = SyntaxHighlighter.Match, + code = match[0], + tag = new XRegExp('(<|<)[\\s\\/\\?]*(?[:\\w-\\.]+)', 'xg').exec(code), + result = [] + ; + + if (match.attributes != null) + { + var attributes, + regex = new XRegExp('(? [\\w:\\-\\.]+)' + + '\\s*=\\s*' + + '(? ".*?"|\'.*?\'|\\w+)', + 'xg'); + + while ((attributes = regex.exec(code)) != null) + { + result.push(new constructor(attributes.name, match.index + attributes.index, 'color1')); + result.push(new constructor(attributes.value, match.index + attributes.index + attributes[0].indexOf(attributes.value), 'string')); + } + } + + if (tag != null) + result.push( + new constructor(tag.name, match.index + tag[0].indexOf(tag.name), 'keyword') + ); + + return result; + } + + this.regexList = [ + { regex: new XRegExp('(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)', 'gm'), css: 'color2' }, // + { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // + { regex: new XRegExp('(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)', 'sg'), func: process } + ]; + }; + + Brush.prototype = new SyntaxHighlighter.Highlighter(); + Brush.aliases = ['xml', 'xhtml', 'xslt', 'html']; + + SyntaxHighlighter.brushes.Xml = Brush; + + // CommonJS + typeof(exports) != 'undefined' ? exports.Brush = Brush : null; +})(); diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shCore.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shCore.js new file mode 100644 index 0000000..b47b645 --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shCore.js @@ -0,0 +1,17 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('K M;I(M)1S 2U("2a\'t 4k M 4K 2g 3l 4G 4H");(6(){6 r(f,e){I(!M.1R(f))1S 3m("3s 15 4R");K a=f.1w;f=M(f.1m,t(f)+(e||""));I(a)f.1w={1m:a.1m,19:a.19?a.19.1a(0):N};H f}6 t(f){H(f.1J?"g":"")+(f.4s?"i":"")+(f.4p?"m":"")+(f.4v?"x":"")+(f.3n?"y":"")}6 B(f,e,a,b){K c=u.L,d,h,g;v=R;5K{O(;c--;){g=u[c];I(a&g.3r&&(!g.2p||g.2p.W(b))){g.2q.12=e;I((h=g.2q.X(f))&&h.P===e){d={3k:g.2b.W(b,h,a),1C:h};1N}}}}5v(i){1S i}5q{v=11}H d}6 p(f,e,a){I(3b.Z.1i)H f.1i(e,a);O(a=a||0;a-1},3d:6(g){e+=g}};c1&&p(e,"")>-1){a=15(J.1m,n.Q.W(t(J),"g",""));n.Q.W(f.1a(e.P),a,6(){O(K c=1;c<14.L-2;c++)I(14[c]===1d)e[c]=1d})}I(J.1w&&J.1w.19)O(K b=1;be.P&&J.12--}H e};I(!D)15.Z.1A=6(f){(f=n.X.W(J,f))&&J.1J&&!f[0].L&&J.12>f.P&&J.12--;H!!f};1r.Z.1C=6(f){M.1R(f)||(f=15(f));I(f.1J){K e=n.1C.1p(J,14);f.12=0;H e}H f.X(J)};1r.Z.Q=6(f,e){K a=M.1R(f),b,c;I(a&&1j e.58()==="3f"&&e.1i("${")===-1&&y)H n.Q.1p(J,14);I(a){I(f.1w)b=f.1w.19}Y f+="";I(1j e==="6")c=n.Q.W(J,f,6(){I(b){14[0]=1f 1r(14[0]);O(K d=0;dd.L-3;){i=1r.Z.1a.W(g,-1)+i;g=1Q.3i(g/10)}H(g?d[g]||"":"$")+i}Y{g=+i;I(g<=d.L-3)H d[g];g=b?p(b,i):-1;H g>-1?d[g+1]:h}})})}I(a&&f.1J)f.12=0;H c};1r.Z.1e=6(f,e){I(!M.1R(f))H n.1e.1p(J,14);K a=J+"",b=[],c=0,d,h;I(e===1d||+e<0)e=5D;Y{e=1Q.3i(+e);I(!e)H[]}O(f=M.3c(f);d=f.X(a);){I(f.12>c){b.U(a.1a(c,d.P));d.L>1&&d.P=e)1N}f.12===d.P&&f.12++}I(c===a.L){I(!n.1A.W(f,"")||h)b.U("")}Y b.U(a.1a(c));H b.L>e?b.1a(0,e):b};M.1h(/\\(\\?#[^)]*\\)/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"});M.1h(/\\((?!\\?)/,6(){J.19.U(N);H"("});M.1h(/\\(\\?<([$\\w]+)>/,6(f){J.19.U(f[1]);J.2N=R;H"("});M.1h(/\\\\k<([\\w$]+)>/,6(f){K e=p(J.19,f[1]);H e>-1?"\\\\"+(e+1)+(3R(f.2S.3a(f.P+f[0].L))?"":"(?:)"):f[0]});M.1h(/\\[\\^?]/,6(f){H f[0]==="[]"?"\\\\b\\\\B":"[\\\\s\\\\S]"});M.1h(/^\\(\\?([5A]+)\\)/,6(f){J.3d(f[1]);H""});M.1h(/(?:\\s+|#.*)+/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"},M.1B,6(){H J.2K("x")});M.1h(/\\./,6(){H"[\\\\s\\\\S]"},M.1B,6(){H J.2K("s")})})();1j 2e!="1d"&&(2e.M=M);K 1v=6(){6 r(a,b){a.1l.1i(b)!=-1||(a.1l+=" "+b)}6 t(a){H a.1i("3e")==0?a:"3e"+a}6 B(a){H e.1Y.2A[t(a)]}6 p(a,b,c){I(a==N)H N;K d=c!=R?a.3G:[a.2G],h={"#":"1c",".":"1l"}[b.1o(0,1)]||"3h",g,i;g=h!="3h"?b.1o(1):b.5u();I((a[h]||"").1i(g)!=-1)H a;O(a=0;d&&a\'+c+""});H a}6 n(a,b){a.1e("\\n");O(K c="",d=0;d<50;d++)c+=" ";H a=v(a,6(h){I(h.1i("\\t")==-1)H h;O(K g=0;(g=h.1i("\\t"))!=-1;)h=h.1o(0,g)+c.1o(0,b-g%b)+h.1o(g+1,h.L);H h})}6 x(a){H a.Q(/^\\s+|\\s+$/g,"")}6 D(a,b){I(a.Pb.P)H 1;Y I(a.Lb.L)H 1;H 0}6 y(a,b){6 c(k){H k[0]}O(K d=N,h=[],g=b.2D?b.2D:c;(d=b.1I.X(a))!=N;){K i=g(d,b);I(1j i=="3f")i=[1f e.2L(i,d.P,b.23)];h=h.1O(i)}H h}6 E(a){K b=/(.*)((&1G;|&1y;).*)/;H a.Q(e.3A.3M,6(c){K d="",h=N;I(h=b.X(c)){c=h[1];d=h[2]}H\'
    \'+c+""+d})}6 z(){O(K a=1E.36("1k"),b=[],c=0;c<1z 4I="1Z://2y.3L.3K/4L/5L"><3J><4N 1Z-4M="5G-5M" 6K="2O/1z; 6J=6I-8" /><1t>6L 1v<3B 1L="25-6M:6Q,6P,6O,6N-6F;6y-2f:#6x;2f:#6w;25-22:6v;2O-3D:3C;">1v3v 3.0.76 (72 73 3x)1Z://3u.2w/1v70 17 6U 71.6T 6X-3x 6Y 6D.6t 61 60 J 1k, 5Z 5R 5V <2R/>5U 5T 5S!\'}},1Y:{2j:N,2A:{}},1U:{},3A:{6n:/\\/\\*[\\s\\S]*?\\*\\//2c,6m:/\\/\\/.*$/2c,6l:/#.*$/2c,6k:/"([^\\\\"\\n]|\\\\.)*"/g,6o:/\'([^\\\\\'\\n]|\\\\.)*\'/g,6p:1f M(\'"([^\\\\\\\\"]|\\\\\\\\.)*"\',"3z"),6s:1f M("\'([^\\\\\\\\\']|\\\\\\\\.)*\'","3z"),6q:/(&1y;|<)!--[\\s\\S]*?--(&1G;|>)/2c,3M:/\\w+:\\/\\/[\\w-.\\/?%&=:@;]*/g,6a:{18:/(&1y;|<)\\?=?/g,1b:/\\?(&1G;|>)/g},69:{18:/(&1y;|<)%=?/g,1b:/%(&1G;|>)/g},6d:{18:/(&1y;|<)\\s*1k.*?(&1G;|>)/2T,1b:/(&1y;|<)\\/\\s*1k\\s*(&1G;|>)/2T}},16:{1H:6(a){6 b(i,k){H e.16.2o(i,k,e.13.1x[k])}O(K c=\'\',d=e.16.2x,h=d.2X,g=0;g";H c},2o:6(a,b,c){H\'<2W>\'+c+""},2b:6(a){K b=a.1F,c=b.1l||"";b=B(p(b,".20",R).1c);K d=6(h){H(h=15(h+"6f(\\\\w+)").X(c))?h[1]:N}("6g");b&&d&&e.16.2x[d].2B(b);a.3N()},2x:{2X:["21","2P"],21:{1H:6(a){I(a.V("2l")!=R)H"";K b=a.V("1t");H e.16.2o(a,"21",b?b:e.13.1x.21)},2B:6(a){a=1E.6j(t(a.1c));a.1l=a.1l.Q("47","")}},2P:{2B:6(){K a="68=0";a+=", 18="+(31.30-33)/2+", 32="+(31.2Z-2Y)/2+", 30=33, 2Z=2Y";a=a.Q(/^,/,"");a=1P.6Z("","38",a);a.2C();K b=a.1E;b.6W(e.13.1x.37);b.6V();a.2C()}}}},35:6(a,b){K c;I(b)c=[b];Y{c=1E.36(e.13.34);O(K d=[],h=0;h(.*?))\\\\]$"),s=1f M("(?<27>[\\\\w-]+)\\\\s*:\\\\s*(?<1T>[\\\\w-%#]+|\\\\[.*?\\\\]|\\".*?\\"|\'.*?\')\\\\s*;?","g");(j=s.X(k))!=N;){K o=j.1T.Q(/^[\'"]|[\'"]$/g,"");I(o!=N&&m.1A(o)){o=m.X(o);o=o.2V.L>0?o.2V.1e(/\\s*,\\s*/):[]}l[j.27]=o}g={1F:g,1n:C(i,l)};g.1n.1D!=N&&d.U(g)}H d},1M:6(a,b){K c=J.35(a,b),d=N,h=e.13;I(c.L!==0)O(K g=0;g")==o-3){m=m.4h(0,o-3);s=R}l=s?m:l}I((i.1t||"")!="")k.1t=i.1t;k.1D=j;d.2Q(k);b=d.2F(l);I((i.1c||"")!="")b.1c=i.1c;i.2G.74(b,i)}}},2E:6(a){w(1P,"4k",6(){e.1M(a)})}};e.2E=e.2E;e.1M=e.1M;e.2L=6(a,b,c){J.1T=a;J.P=b;J.L=a.L;J.23=c;J.1V=N};e.2L.Z.1q=6(){H J.1T};e.4l=6(a){6 b(j,l){O(K m=0;md)1N;Y I(g.P==c.P&&g.L>c.L)a[b]=N;Y I(g.P>=c.P&&g.P\'+c+""},3Q:6(a,b){K c="",d=a.1e("\\n").L,h=2u(J.V("2i-1s")),g=J.V("2z-1s-2t");I(g==R)g=(h+d-1).1q().L;Y I(3R(g)==R)g=0;O(K i=0;i\'+j+"":"")+i)}H a},4f:6(a){H a?"<4a>"+a+"":""},4b:6(a,b){6 c(l){H(l=l?l.1V||g:g)?l+" ":""}O(K d=0,h="",g=J.V("1D",""),i=0;i|&1y;2R\\s*\\/?&1G;/2T;I(e.13.46==R)b=b.Q(h,"\\n");I(e.13.44==R)b=b.Q(h,"");b=b.1e("\\n");h=/^\\s*/;g=4Q;O(K i=0;i0;i++){K k=b[i];I(x(k).L!=0){k=h.X(k);I(k==N){a=a;1N a}g=1Q.4q(k[0].L,g)}}I(g>0)O(i=0;i\'+(J.V("16")?e.16.1H(J):"")+\'<3Z 5z="0" 5H="0" 5J="0">\'+J.4f(J.V("1t"))+"<3T><3P>"+(1u?\'<2d 1g="1u">\'+J.3Q(a)+"":"")+\'<2d 1g="17">\'+b+""},2F:6(a){I(a===N)a="";J.17=a;K b=J.3Y("T");b.3X=J.1H(a);J.V("16")&&w(p(b,".16"),"5c",e.16.2b);J.V("3V-17")&&w(p(b,".17"),"56",f);H b},2Q:6(a){J.1c=""+1Q.5d(1Q.5n()*5k).1q();e.1Y.2A[t(J.1c)]=J;J.1n=C(e.2v,a||{});I(J.V("2k")==R)J.1n.16=J.1n.1u=11},5j:6(a){a=a.Q(/^\\s+|\\s+$/g,"").Q(/\\s+/g,"|");H"\\\\b(?:"+a+")\\\\b"},5f:6(a){J.28={18:{1I:a.18,23:"1k"},1b:{1I:a.1b,23:"1k"},17:1f M("(?<18>"+a.18.1m+")(?<17>.*?)(?<1b>"+a.1b.1m+")","5o")}}};H e}();1j 2e!="1d"&&(2e.1v=1v);',62,441,'||||||function|||||||||||||||||||||||||||||||||||||return|if|this|var|length|XRegExp|null|for|index|replace|true||div|push|getParam|call|exec|else|prototype||false|lastIndex|config|arguments|RegExp|toolbar|code|left|captureNames|slice|right|id|undefined|split|new|class|addToken|indexOf|typeof|script|className|source|params|substr|apply|toString|String|line|title|gutter|SyntaxHighlighter|_xregexp|strings|lt|html|test|OUTSIDE_CLASS|match|brush|document|target|gt|getHtml|regex|global|join|style|highlight|break|concat|window|Math|isRegExp|throw|value|brushes|brushName|space|alert|vars|http|syntaxhighlighter|expandSource|size|css|case|font|Fa|name|htmlScript|dA|can|handler|gm|td|exports|color|in|href|first|discoveredBrushes|light|collapse|object|cache|getButtonHtml|trigger|pattern|getLineHtml|nbsp|numbers|parseInt|defaults|com|items|www|pad|highlighters|execute|focus|func|all|getDiv|parentNode|navigator|INSIDE_CLASS|regexList|hasFlag|Match|useScriptTags|hasNamedCapture|text|help|init|br|input|gi|Error|values|span|list|250|height|width|screen|top|500|tagName|findElements|getElementsByTagName|aboutDialog|_blank|appendChild|charAt|Array|copyAsGlobal|setFlag|highlighter_|string|attachEvent|nodeName|floor|backref|output|the|TypeError|sticky|Za|iterate|freezeTokens|scope|type|textarea|alexgorbatchev|version|margin|2010|005896|gs|regexLib|body|center|align|noBrush|require|childNodes|DTD|xhtml1|head|org|w3|url|preventDefault|container|tr|getLineNumbersHtml|isNaN|userAgent|tbody|isLineHighlighted|quick|void|innerHTML|create|table|links|auto|smart|tab|stripBrs|tabs|bloggerMode|collapsed|plain|getCodeLinesHtml|caption|getMatchesHtml|findMatches|figureOutLineNumbers|removeNestedMatches|getTitleHtml|brushNotHtmlScript|substring|createElement|Highlighter|load|HtmlScript|Brush|pre|expand|multiline|min|Can|ignoreCase|find|blur|extended|toLowerCase|aliases|addEventListener|innerText|textContent|wasn|select|createTextNode|removeChild|option|same|frame|xmlns|dtd|twice|1999|equiv|meta|htmlscript|transitional|1E3|expected|PUBLIC|DOCTYPE|on|W3C|XHTML|TR|EN|Transitional||configured|srcElement|Object|after|run|dblclick|matchChain|valueOf|constructor|default|switch|click|round|execAt|forHtmlScript|token|gimy|functions|getKeywords|1E6|escape|within|random|sgi|another|finally|supply|MSIE|ie|toUpperCase|catch|returnValue|definition|event|border|imsx|constructing|one|Infinity|from|when|Content|cellpadding|flags|cellspacing|try|xhtml|Type|spaces|2930402|hosted_button_id|lastIndexOf|donate|active|development|keep|to|xclick|_s|Xml|please|like|you|paypal|cgi|cmd|webscr|bin|highlighted|scrollbars|aspScriptTags|phpScriptTags|sort|max|scriptScriptTags|toolbar_item|_|command|command_|number|getElementById|doubleQuotedString|singleLinePerlComments|singleLineCComments|multiLineCComments|singleQuotedString|multiLineDoubleQuotedString|xmlComments|alt|multiLineSingleQuotedString|If|https|1em|000|fff|background|5em|xx|bottom|75em|Gorbatchev|large|serif|CDATA|continue|utf|charset|content|About|family|sans|Helvetica|Arial|Geneva|3em|nogutter|Copyright|syntax|close|write|2004|Alex|open|JavaScript|highlighter|July|02|replaceChild|offset|83'.split('|'),0,{})) diff --git a/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shLegacy.js b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shLegacy.js new file mode 100644 index 0000000..6d9fd4d --- /dev/null +++ b/addons/--Syntax_Highlighting/syntaxhighlighter/scripts/shLegacy.js @@ -0,0 +1,17 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('3 u={8:{}};u.8={A:4(c,k,l,m,n,o){4 d(a,b){2 a!=1?a:b}4 f(a){2 a!=1?a.E():1}c=c.I(":");3 g=c[0],e={};t={"r":K};M=1;5=8.5;9(3 j R c)e[c[j]]="r";k=f(d(k,5.C));l=f(d(l,5.D));m=f(d(m,5.s));o=f(d(o,5.Q));n=f(d(n,5["x-y"]));2{P:g,C:d(t[e.O],k),D:d(t[e.N],l),s:d({"r":r}[e.s],m),"x-y":d(4(a,b){9(3 h=T S("^"+b+"\\\\[(?\\\\w+)\\\\]$","U"),i=1,p=0;p
    '; + + return $output; + } + + + public function GetNewSection($type){ + + $class = self::TypeClass($type); + $num = time().rand(0, 10000); + $new_section = \gp\tool\Editing::DefaultContent($type); + $content = \gp\tool\Output\Sections::RenderSection($new_section, $num, $this->title, $this->file_stats); + + $new_section['attributes']['class'] .= ' ' . $class; + $new_section['gp_type'] = $type; + $orig_attrs = $new_section['attributes']; + + if( !isset($new_section['nodeName']) ){ + return $this->SectionNode($new_section, $orig_attrs) . $content . '
    '; + } + + return $this->SectionNode($new_section, $orig_attrs) . + $content . + \gp\tool\Output\Sections::EndTag($new_section['nodeName']); + } + + + public function SectionNode($section, $orig_attrs){ + + //if image type, make sure the src is a complete path + if( $section['type'] == 'image' ){ + $orig_attrs['src'] = \gp\tool::GetDir($orig_attrs['src']); + } + + $orig_attrs = json_encode($orig_attrs); + + if( \gp\tool\Output::ShowEditLink() && \gp\admin\Tools::CanEdit($this->gp_index) ){ + $section['attributes']['class'] .= ' editable_area'; + } + + $section_attrs = ['gp_label', 'gp_color', 'gp_collapse', 'gp_type', 'gp_hidden', 'gp_file_under']; + foreach($section_attrs as $attr){ + if( !empty($section[$attr]) ){ + $section['attributes']['data-' . $attr] = $section[$attr]; + } + } + + $attributes = \gp\tool\Output\Sections::SectionAttributes($section['attributes'], $section['type']); + $attributes .= ' data-gp-attrs=\'' . htmlspecialchars($orig_attrs, ENT_QUOTES & ~ENT_COMPAT) . '\''; + + + if( !isset($section['nodeName']) ){ + return ''; + } + + return '<' . $section['nodeName'] . $attributes . '>'; + } + + + /** + * Save new/rearranged sections + * + */ + public function SaveSections(){ + global $langmessage, $dataDir; + + if( isset($_POST['sections_json']) ){ + // section data is posted as JSON + $section_data = json_decode(rawurldecode($_POST['sections_json']), true); + $_POST['section_data_is'] = 'json'; + $_POST += $section_data; + unset($_POST['sections_json']); + } + + $this->ajaxReplace = []; + $original_sections = $this->file_sections; + $unused_sections = $this->file_sections; //keep track of sections that aren't used + $new_sections = []; + + //make sure section_order isn't empty + if( empty($_POST['section_order']) ){ + msg($langmessage['OOPS'] . ' (Invalid Request)'); + return false; + } + + foreach($_POST['section_order'] as $i => $arg ){ + $new_section = $this->SaveSection($i, $arg, $unused_sections); + if( $new_section === false ){ + return false; + } + $new_sections[$i] = $new_section; + } + + //make sure there's at least one section + if( empty($new_sections) ){ + msg($langmessage['OOPS'] . ' (1 Section Minimum)'); + return false; + } + + $this->file_sections = array_values($new_sections); + + // save a send message to user + if( !$this->SaveThis() ){ + $this->file_sections = $original_sections; + msg($langmessage['OOPS'] . '(4)'); + return; + } + + $this->ajaxReplace[] = ['ck_saved', '', '']; + + //update gallery info + $this->GalleryEdited(); + + //update usage of resized images + foreach($unused_sections as $section_data){ + if( isset($section_data['resized_imgs']) ){ + includeFile('image.php'); + \gp_resized::SetIndex(); + \gp\tool\Editing::ResizedImageUse($section_data['resized_imgs'], []); + } + } + } + + + protected function SaveSection($i, $arg, &$unused_sections){ + global $langmessage; + + $section_attrs = ['gp_label', 'gp_color', 'gp_collapse', 'gp_type', 'gp_hidden', 'gp_file_under']; + + // moved / copied sections + if( ctype_digit($arg) ){ + $arg = (int)$arg; + + if( !isset($this->file_sections[$arg]) ){ + msg($langmessage['OOPS'] . ' (Invalid Section Number)'); + return false; + } + + unset($unused_sections[$arg]); + $new_section = $this->file_sections[$arg]; + $new_section['attributes'] = []; + + // otherwise, new sections + }else{ + $new_section = \gp\tool\Editing::DefaultContent($arg); + } + + // attributes + $this->PostedAttributes($new_section,$i); + + // wrapper section 'contains_sections' + if( $new_section['type'] == 'wrapper_section' ){ + $new_section['contains_sections'] = isset($_POST['contains_sections']) ? $_POST['contains_sections'][$i] : '0'; + } + + // section attributes + foreach($section_attrs as $attr){ + unset($new_section[$attr]); + if( !empty($_POST[$attr][$i]) ){ + $new_section[$attr] = $_POST[$attr][$i]; + } + } + + return $new_section; + } + + + /** + * Save a section or wrapper with sections to the Clipboard + * + */ + protected function SaveToClipboard(){ + global $langmessage; + $this->ajaxReplace = []; + + if( !isset($_POST['section_number']) || !ctype_digit($_POST['section_number']) ){ + msg($langmessage['OOPS'] . ' (SaveToClipboard: Invalid Request)'); + return false; + } + + $section_num = $_POST['section_number']; + if( !isset($this->file_sections[$section_num]) ){ + msg($langmessage['OOPS'] . ' (SaveToClipboard: Invalid Section Number (' . $section_num . ')'); + } + + $file_sections = self::ExtractSections($section_num); + // msg('SaveToClipboard: $file_sections = ' . pre($file_sections) ); + + $first_section = $file_sections[0]; + $new_clipboard_item = [ + 'type' => $first_section['type'], + 'label' => isset($first_section['gp_label']) ? $first_section['gp_label'] : ucfirst($first_section['type']), + 'color' => isset($first_section['gp_color']) ? $first_section['gp_color'] : '#aabbcc', + 'hidden' => isset($first_section['gp_hidden']) ? $first_section['gp_hidden'] : false, + 'hidden' => isset($first_section['gp_file_under']) ? $first_section['gp_file_under'] : '', + 'content' => $this->GetSectionForClipboard($section_num), + 'file_sections' => $file_sections, + ]; + + $clipboard_data = \gp\tool\Files::GetSectionClipboard(); + // msg("GetSectionClipboard returns " . pre($clipboard_data)); + array_unshift($clipboard_data, $new_clipboard_item); + if( \gp\tool\Files::SaveSectionClipboard($clipboard_data) ){ + // msg($langmessage['SAVED']); + }else{ + msg($langmessage['OOPS'] . ' (Section Clipboard: Save data failed)'); + } + + $clipboard_links = self::SectionClipboardLinks($clipboard_data); + $this->ajaxReplace[] = ['inner', '#section-clipboard-items', $clipboard_links]; + $this->ajaxReplace[] = ['clipboard_init', '', '']; + $this->ajaxReplace[] = ['loaded', '', '']; + + return true; + } + + + /** + * Add (append) a Clipboard Item to the page's file sections + * + */ + protected function AddFromClipboard($item_num=false){ + global $langmessage; + $this->ajaxReplace = []; + + if( !$item_num ){ + if( !isset($_POST['item_number']) || !ctype_digit($_POST['item_number']) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Add Item: Invalid Request'); + return false; + } + $item_num = $_POST['item_number']; + } + + $clipboard_data = \gp\tool\Files::GetSectionClipboard(); + + if( !isset($clipboard_data[$item_num]) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Add Item: Invalid Item Number (' . $item_num . ')'); + return false; + } + + $clipboard_sections = $clipboard_data[$item_num]['file_sections']; + $this->file_sections = array_merge($this->file_sections, $clipboard_sections); + + if( !$this->SaveThis() ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Add Item: Save Page Failed)'); + $this->ajaxReplace[] = ['loaded', '', '']; + return false; + } + + // $this->ajaxReplace[] = ['ck_saved', '', '']; + + // include updated Admin Toolbar + ob_start(); + \gp\admin\Tools::AdminToolbar(); + $admin_toolbar = ob_get_clean(); + if( !empty($admin_toolbar) ){ + $this->ajaxReplace[] = ['replace', '#admincontent_panel', $admin_toolbar]; + } + $this->ajaxReplace[] = ['loaded', '', '']; + return; + } + + + /** + * Remove a Clipboard Item + * + */ + protected function RemoveFromClipboard($item_num=false){ + global $langmessage; + $this->ajaxReplace = []; + + if( !$item_num ){ + if( !isset($_POST['item_number']) || !ctype_digit($_POST['item_number']) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Remove Item: Invalid Request'); + return false; + } + $item_num = $_POST['item_number']; + } + + $clipboard_data = \gp\tool\Files::GetSectionClipboard(); + + if( !isset($clipboard_data[$item_num]) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Remove Item: Invalid Item Number (' . $item_num . ')'); + return false; + } + unset($clipboard_data[$item_num]); + $clipboard_data = array_values($clipboard_data); + + if( \gp\tool\Files::SaveSectionClipboard($clipboard_data) ){ + // msg($langmessage['SAVED']); + }else{ + msg($langmessage['OOPS'] . ' (Section Clipboard - Remove Item: Save data failed)'); + return false; + } + + $clipboard_links = self::SectionClipboardLinks($clipboard_data); + $this->ajaxReplace[] = ['inner', '#section-clipboard-items', $clipboard_links]; + $this->ajaxReplace[] = ['clipboard_init', '', '']; + $this->ajaxReplace[] = ['loaded', '', '']; + + return true; + } + + + /** + * Change the Clipboard Items' order + * + */ + protected function ReorderClipboardItems($order=false){ + global $langmessage; + $this->ajaxReplace = []; + + if( !is_array($order) ){ + if( !is_array($_POST['order']) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Reorder Items: Invalid Request'); + return false; + } + $order = $_POST['order']; + } + + $clipboard_data = \gp\tool\Files::GetSectionClipboard(); + + if( count($order) != count($clipboard_data) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Reorder Items: Item count mismatch'); + return false; + } + + foreach( $order as $key => $index ){ + $new_clipboard_data[$key] = $clipboard_data[$index]; + } + + $clipboard_data = $new_clipboard_data; + unset($new_clipboard_data); + + if( \gp\tool\Files::SaveSectionClipboard($clipboard_data) ){ + // msg($langmessage['SAVED']); + }else{ + msg($langmessage['OOPS'] . ' (Section Clipboard - Reorder Items: Save data failed)'); + return false; + } + + $clipboard_links = self::SectionClipboardLinks($clipboard_data); + $this->ajaxReplace[] = ['inner', '#section-clipboard-items', $clipboard_links]; + $this->ajaxReplace[] = ['loaded', '', '']; + + return true; + } + + + /** + * Change the label of a Clipboard Item + * + */ + protected function RelabelClipboardItem($item_num=false,$new_label=false){ + global $langmessage; + $this->ajaxReplace = []; + + if( !$item_num ){ + if( !isset($_POST['item_number']) || !ctype_digit($_POST['item_number']) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Remove Item: Invalid Request'); + return false; + } + $item_num = $_POST['item_number']; + } + + if( !$new_label ){ + if( !isset($_POST['new_label']) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Relabel Item: Invalid Label'); + return false; + } + $new_label = htmlspecialchars($_POST['new_label']); + } + + $clipboard_data = \gp\tool\Files::GetSectionClipboard(); + + if( !isset($clipboard_data[$item_num]) ){ + msg($langmessage['OOPS'] . ' (Section Clipboard - Relabel Item: Invalid Item Number (' . $item_num . ')'); + return false; + } + + $clipboard_data = array_values($clipboard_data); + + $clipboard_data[$item_num]['label'] = $new_label; + + if( \gp\tool\Files::SaveSectionClipboard($clipboard_data) ){ + // msg($langmessage['SAVED']); + }else{ + msg($langmessage['OOPS'] . ' (Section Clipboard - Remove Item: Save data failed)'); + return false; + } + + $clipboard_links = self::SectionClipboardLinks($clipboard_data); + $this->ajaxReplace[] = ['inner', '#section-clipboard-items', $clipboard_links]; + $this->ajaxReplace[] = ['loaded', '', '']; + + return true; + } + + + /** + * Extract sections from the current page to be stored in the Clipboard + * TS 5.1.1-b1: fix nesting error, changing from recursion to while iteration + counter + */ + public function ExtractSections($section_num){ + $counter = 0; + $sections = []; + while( $counter >= 0 ){ + $section_data = $this->file_sections[$section_num]; + + if( $section_data['type'] == 'wrapper_section' && isset($section_data['contains_sections']) ){ + $counter += $section_data['contains_sections']; + } + + // remove possible hidden state + $section_data['gp_hidden'] = false; + + // add section + $sections[] = $section_data; + + $section_num++; + $counter--; + } + + return $sections; + } + + + + /** + * Get the Section Clipboard links + * + */ + public static function SectionClipboardLinks($clipboard_data=false){ + global $langmessage; + $clipboard_data = $clipboard_data ? $clipboard_data : \gp\tool\Files::GetSectionClipboard(); + + if( empty($clipboard_data) ){ + return '
  • ' . $langmessage['Clipboard'] . '
  • '; + } + + $clipboard_links = ''; + + foreach( $clipboard_data as $key => $clipboard_item ){ + $icon_class = $clipboard_item['type'] == 'wrapper_section' ? 'fa fa-fw fa-clone' : 'fa fa-fw fa-square-o'; + // $icon_title = $clipboard_item['label']; + // $content = \gp\tool\Output\Sections::GetSection($clipboard_item['file_sections'], 0); // private method :-( + $content = $clipboard_item['content']; + $response = htmlspecialchars($content); + + $clipboard_links .= '
  • '; + + $clipboard_links .= ''; + $clipboard_links .= ' '; + $clipboard_links .= ''; + $clipboard_links .= '' . $clipboard_item['label'] . ''; + $clipboard_links .= ''; + $clipboard_links .= ''; + + $clipboard_links .= '
  • '; + } + + return $clipboard_links; + } + + + /** + * Get the posted attributes for a section + * + */ + protected function PostedAttributes(&$section, $i){ + global $dirPrefix; + + if( isset($_POST['attributes'][$i]) && is_array($_POST['attributes'][$i]) ){ + foreach($_POST['attributes'][$i] as $attr_name => $attr_value){ + + $attr_name = strtolower($attr_name); + $attr_name = trim($attr_name); + $attr_value = trim($attr_value); + + if( empty($attr_name) || empty($attr_value) || $attr_name == 'id' || substr($attr_name, 0, 7) == 'data-gp' ){ + continue; + } + + //strip $dirPrefix + if( $attr_name == 'src' && !empty($dirPrefix) && strpos($attr_value,$dirPrefix) === 0 ){ + $attr_value = substr($attr_value, strlen($dirPrefix)); + } + + $section['attributes'][$attr_name] = $attr_value; + } + } + } + + + /** + * Perform various section editing commands + * @return bool + */ + public function SectionEdit(){ + global $langmessage, $page; + + $page->ajaxReplace = []; + + $section_num = $_REQUEST['section']; + if( !is_numeric($section_num) || !isset($this->file_sections[$section_num])){ + echo 'false;'; + return false; + } + + $cmd = \gp\tool::GetCommand(); + + if( !\gp\tool\Editing::SectionEdit($cmd, $this->file_sections[$section_num], $section_num, $this->title, $this->file_stats) ){ + return false; + } + + //save if the file was changed + if( !$this->SaveThis() ){ + msg($langmessage['OOPS'].' (SE3)'); + return false; + } + + $page->ajaxReplace[] = ['ck_saved', '', '']; + + //update gallery information + switch($this->file_sections[$section_num]['type']){ + case 'gallery': + $this->GalleryEdited(); + break; + } + + \gp\admin\Notifications::UpdateNotifications(); + + return true; + } + + + /** + * Recalculate the file_type string for this file + * Updates $this->meta_data and $gp_titles + * + */ + public function ResetFileTypes(){ + global $gp_titles; + + $new_types = []; + foreach($this->file_sections as $section){ + $new_types[] = $section['type']; + } + $new_types = array_unique($new_types); + $new_types = array_diff($new_types, ['']); + sort($new_types); + + $new_types = implode(',', $new_types); + $this->meta_data['file_type'] = $new_types; + + if( !isset($gp_titles[$this->gp_index]) ){ + return; + } + + $gp_titles[$this->gp_index]['type'] = $new_types; + \gp\admin\Tools::SavePagesPHP(); + } + + + /** + * Return a list of section types + * + */ + public static function NewSections($checkboxes=false){ + global $langmessage; + + $types_with_icons = ['text', 'image', 'gallery', 'wrapper_section', 'include']; + + $section_types = \gp\tool\Output\Sections::GetTypes(); + + $links = []; + + foreach($section_types as $type => $type_info){ + $img = ''; + if( in_array($type, $types_with_icons) ){ + $img = \gp\tool::GetDir('/include/imgs/section-' . $type . '.png'); + } + $links[] = [$type, $img, '', $type_info['file_under']]; + } + + //section combo: text & image + $links[] = [ + ['text.gpCol-6', 'image.gpCol-6'], + \gp\tool::GetDir('/include/imgs/section-combo-text-image.png'), + [ + 'gp_label' => 'Text & Image', + 'gp_color' => '#555', + 'attributes' => [ + 'class' => 'gpRow', + ], + ], + 'default', // file_under, req as of TS 5.2 + ]; + + //section combo: text & gallery + $links[] = [ + ['text.gpCol-6', 'gallery.gpCol-6'], + \gp\tool::GetDir('/include/imgs/section-combo-text-gallery.png'), + [ + 'gp_label' => 'Text & Gallery', + 'gp_color' => '#555', + 'attributes' => [ + 'class' => 'gpRow', + ], + ], + 'default', + ]; + + //section combo: 3 text columns + $links[] = [ + ['text.gpCol-4', 'text.gpCol-4', 'text.gpCol-4'], + \gp\tool::GetDir('/include/imgs/section-combo-3-text-cols.png'), + [ + 'gp_label' => '3 Text Columns', + 'gp_color' => '#555', + 'attributes' => [ + 'class' => 'gpRow', + ], + ], + 'default', + ]; + + /** + * FYI when using the 'NewSections' plugin filter hook + * + * a new section subarray looks like this: + * + * array( + * [0] => (string)section type OR (array) of (string)section types (or subsequent wrapper arrays) + * if we pass an array, the new section is a wrapper (combo) containing nested sections + * while every (string)section type MAY contain one or multiple css classes + * css classes are everything in (string)section type after a dot + * multiple css classes are separated by space charaters + * + * [1] => (string) relative path to an admin UI icon for that section type + * if empty, no icon will appear + * + * [2] => (boolean)false OR (string)wrapper class name(s) OR (array)wrapper data + * when passing wrapper classes string, multiple class names are separated by space characters + * when passing a wrapper data array, it may contain arbitrary section data key-pairs + * including an array of html attributes like 'class' + * + * [3] => as of Typesetter 5.2, optional (string)file_under + * use 'hidden' if you do not want the link to appear in the admi UI + * will become 'plugins' by default if not defined + * ) + * + * Furthermore: instead of existing 'real' section types, you can also use pseudo types + * pseudo types must to be defined via the GetDefaultContent filter hook + * where a section array must be returned, which has the real section type string in the 'type' key + * + * This is a grown structure and we apologize that it is so messy + * + * For handling more complex new sections and even multi-level-nested section combos see e.g. + * the SliderFactory plugin (https://www.typesettercms.com/Plugins/310_Slider_Factory) + * + */ + $links = \gp\tool\Plugins::Filter('NewSections', [$links]); + + $new_section_links = []; + foreach( $links as $link ){ + $link += ['', '', false, 'plugins']; // $link[2] will be replaced in NewSectionLink() if missing + $type_id = substr(base_convert(md5(json_encode((array)$link[0])), 16, 32), 0, 6); // creates a unique id for every entry + $new_section_links[$type_id] = $link; + } + + // last chance to filter, re-arrange or modify new section links + // before they appear in the admin UI (based on their $type_id or other criteria) + $new_section_links = \gp\tool\Plugins::Filter('NewSectionLinks', [$new_section_links]); + + $stackers = []; + + foreach( $new_section_links as $type_id => $link ){ + $file_under = $link[3]; + $stackers[$file_under][$type_id] = $link; + } + + // remove all section types that are filed under 'hidden' + unset($stackers['hidden']); + + // if there are more than 12 links and more than 1 remaining stackers, split the output into an accordion + $use_stackers = count($stackers) > 1 && $new_section_links > 12; + + $stacker_collapsed = false; + foreach( $stackers as $stacker => $links ){ + if( $use_stackers ){ + $stacker_label = isset($langmessage[$stacker]) ? $langmessage[$stacker] : htmlspecialchars($stacker); + echo '
    '; + echo '

    '; + echo '' . $stacker_label . ''; + echo '

    '; + echo '
    '; + } + foreach( $links as $link ){ + $types = $link[0]; + $icon = $link[1]; + $wrapper_data = $link[2]; + $file_under = $link[3]; + + echo self::NewSectionLink($types, $icon, $wrapper_data, $checkboxes, $type_id, $file_under); + } + if( $use_stackers ){ + echo '
    '; + echo '
    '; + $stacker_collapsed = true; + } + + } + } + + + /** + * Add link to manage section admin for nested section type + * + */ + public static function NewSectionLink($types, $icon, $wrapper_data=false, $checkbox=false, $type_id='undefined', $file_under=''){ + global $dataDir, $page, $langmessage; + + $types = (array)$types; + + $is_wrapper = count([$types]) > 1 || is_array($types[0]); + + if( $is_wrapper && !$wrapper_data ){ + // add default wrapper data if undefined + $wrapper_data = [ + 'gp_label' => $langmessage['Section Wrapper'], + 'gp_color' => '#555', + 'attributes' => [ + // 'class' => 'gpRow', // we don't use gpRow as default class for new wrappers anymore + ], + ]; + } + + static $fi = 0; + + $text_label = $is_wrapper && isset($wrapper_data['gp_label']) ? $wrapper_data['gp_label'] : self::SectionLabel($types); + + $label = '' . $text_label . ''; + if( !empty($icon) ){ + $label .= ''; + } + + //checkbox used for new pages + if( $checkbox ){ + + if( count($types) > 1 || is_array($types[0]) ){ // == nested sections + $q = ['types' => $types, 'wrapper_data' => $wrapper_data]; + $q = json_encode($q); + }else{ + $q = $types[0]; + } + + //checked + $checked = ''; + if( isset($_REQUEST['content_type']) && $_REQUEST['content_type'] == $q ){ + $checked = ' checked="checked"'; + }elseif( empty($_REQUEST['content_type']) && $fi === 0 ){ + $checked = ' checked="checked"'; + $fi++; + } + + $id = 'checkbox_' . md5($q); + echo ''; + return; + } // /if $checkboxes + + //links used for new sections + $attrs = [ + 'data-cmd' => 'AddSection', + 'class' => 'preview_section', + ]; + if( count($types) > 1 || is_array($types[0]) ){ + $attrs['data-response'] = $page->NewNestedSection($types, $wrapper_data); + }else{ + $attrs['data-response'] = $page->GetNewSection($types[0]); + } + + $return = ''; + + return $return; + } + + + /** + * Return a readable label for the section + * + */ + public static function SectionLabel($types){ + $section_types = \gp\tool\Output\Sections::GetTypes(); + $text_label = []; + + foreach($types as $type){ + + if( is_array($type) ){ + continue; + } + self::TypeClass($type); + + if( isset($section_types[$type]) ){ + $text_label[] = $section_types[$type]['label']; + }else{ + $text_label[] = $type; + } + } + + return implode(' & ', $text_label); + } + + + /** + * Split the type and class from $type = div.classname into $type = div, $class = classname + * + */ + public static function TypeClass(&$type){ + + $class = ''; + + if( !is_array($type) && strpos($type, '.') ){ + list($type,$class) = explode('.', $type, 2); + } + + return $class; + } + + + /** + * Save the current page + * Save a backup if $backup is true + * + */ + public function SaveThis($backup=true){ + + //return true if nothing has changed + if( $backup && $this->checksum === $this->Checksum() ){ + return true; + } + + //file count + if( !isset($this->meta_data['file_number']) ){ + $this->meta_data['file_number'] = \gp\tool\Files::NewFileNumber(); + } + + if( $backup ){ + $this->SaveBackup(); //make a backup of the page file + } + + if( isset($_POST['prevent_draft']) && !$this->draft_exists ){ + if( !\gp\tool\Files::SaveData($this->file, 'file_sections', $this->file_sections, $this->meta_data) ){ + return false; + } + }else{ + if( !\gp\tool\Files::SaveData($this->draft_file, 'file_sections', $this->file_sections, $this->meta_data) ){ + return false; + } + $this->draft_exists = true; + } + + // update notifications + \gp\admin\Notifications::UpdateNotifications(); + + return true; + } + + + /** + * Generate a checksum for this page, used to determine if the page content has been edited + * + */ + public function Checksum(){ + $temp = []; + foreach($this->file_sections as $section){ + unset($section['modified'], $section['modified_by']); + $temp[] = $section; + } + $checksum = serialize($temp); + return sha1($checksum) . md5($checksum); + } + + + /** + * Save a backup of the file + * + */ + public function SaveBackup(){ + global $dataDir, $gpAdmin; + + $dir = $dataDir . '/data/_backup/pages/' . $this->gp_index; + $time = \gp\tool\Editing::ReqTime(); //use the request time + + //just one backup per edit session (auto-saving would create too many backups otherwise) + $previous_backup = $this->BackupFile($time); + if( !empty($previous_backup) ){ + return true; + } + + // get the raw contents so we can add the size to the backup file name + if( $this->draft_exists ){ + $contents = \gp\tool\Files::GetRaw($this->draft_file); + }else{ + $contents = \gp\tool\Files::GetRaw($this->file); + } + + //backup file name + $len = strlen($contents); + $backup_file = $dir . '/' . $time . '.' . $len . '.' . $gpAdmin['username']; + + //compress + if( function_exists('gzencode') && function_exists('readgzfile') ){ + $backup_file .= '.gze'; + $contents = gzencode($contents,9); + } + + if( !\gp\tool\Files::Save($backup_file, $contents) ){ + return false; + } + + $this->CleanBackupFolder(); + return true; + } + + + /** + * Reduce the number of files in the backup folder + * + */ + public function CleanBackupFolder(){ + global $dataDir, $config; + + $files = $this->BackupFiles(); + $file_count = count($files); + + if( $file_count <= $config['history_limit'] ){ + return; + } + $delete_count = $file_count - $config['history_limit']; + $files = array_splice($files, 0, $delete_count); + foreach($files as $file){ + $full_path = $dataDir . '/data/_backup/pages/' . $this->gp_index . '/' . $file; + unlink($full_path); + } + } + + + /** + * Make the working draft the live file + * + */ + public function PublishDraft(){ + global $langmessage, $page; + + if( !$this->draft_exists ){ + msg($langmessage['OOPS'] . ' (Not a draft)'); + return false; + } + + if( !\gp\tool\Files::SaveData($this->file, 'file_sections', $this->file_sections, $this->meta_data) ){ + msg($langmessage['OOPS'] . ' (Draft not published)'); + return false; + } + + $draft_file = \gp\tool\Files::FilePath($this->draft_file); + unlink($draft_file); + $this->ResetFileTypes(); + $this->draft_exists = false; + + $page->ajaxReplace = []; + $page->ajaxReplace[] = ['DraftPublished']; + + \gp\admin\Notifications::UpdateNotifications(); + + return true; + } + + + /** + * Display the contents of a past revision + * + */ + protected function ViewRevision(){ + + \gp\admin\Tools::$show_toolbar = false; + $revision =& $_REQUEST['revision']; + $file_sections = $this->GetRevision($revision); + + $this->head_js[] = '/include/js/admin/revision.js'; + + if( $file_sections === false ){ + $this->DefaultDisplay(); + return; + } + + + echo \gp\tool\Output\Sections::Render($file_sections, $this->title, \gp\tool\Files::$last_stats); + } + + + /** + * View the current public facing version of the file + * + */ + public function ViewCurrent(){ + + \gp\admin\Tools::$show_toolbar = false; + $this->head_js[] = '/include/js/admin/revision.js'; + + if( !$this->draft_exists ){ + $this->DefaultDisplay(); + return; + } + + $file_sections = \gp\tool\Files::Get($this->file, 'file_sections'); + echo \gp\tool\Output\Sections::Render($file_sections, $this->title, $this->file_stats); + } + + + /** + * Get the contents of a revision + * + */ + protected function GetRevision($time){ + + $full_path = $this->BackupFile($time); + + if( is_null($full_path) ){ + return false; + } + + //if it's a compressed file, we need an uncompressed version + if( strpos($full_path, '.gze') !== false ){ + + ob_start(); + readgzfile($full_path); + $contents = ob_get_clean(); + + $full_path = substr($full_path, 0, -3) . 'php'; + $full_path = \gp\tool\Files::FilePath($full_path); + \gp\tool\Files::Save($full_path, $contents); + $file_sections = \gp\tool\Files::Get($full_path, 'file_sections'); + unlink($full_path); + + }else{ + $file_sections = \gp\tool\Files::Get($full_path, 'file_sections'); + } + + return $file_sections; + } + + + /** + * Return a list of the available backup for the current file + * + */ + public function BackupFiles(){ + global $dataDir; + $dir = $dataDir . '/data/_backup/pages/' . $this->gp_index; + + if( !file_exists($dir) ){ + return []; + } + $all_files = scandir($dir); + $files = []; + foreach($all_files as $file){ + if( $file == '.' || $file == '..' ){ + continue; + } + $parts = explode('.', $file); + $time = array_shift($parts); + if( !is_numeric($time) ){ + continue; + } + $files[$time] = $file; + } + + ksort($files); + return $files; + } + + + /** + * Return the full path of the saved revision if it exists + * + */ + public function BackupFile( $time ){ + global $dataDir; + + $files = $this->BackupFiles(); + if( !isset($files[$time]) ){ + return; + } + + return $dataDir . '/data/_backup/pages/' . $this->gp_index . '/' . $files[$time]; + } + + + /** + * Extract information about the gallery from it's html: img_count, icon_src + * Call GalleryEdited when a gallery section is removed, edited + * + */ + public function GalleryEdited(){ + \gp\special\Galleries::UpdateGalleryInfo($this->title, $this->file_sections); + } + + + public function GetSection(&$section_num){ + global $langmessage; + + if( !isset($this->file_sections[$section_num]) ){ + trigger_error('invalid section number'); + return; + } + + $curr_section_num = $section_num; + $section_num++; + + $content = ''; + $section_data = $this->file_sections[$curr_section_num]; + + //make sure section_data is an array + $type = gettype($section_data); + if( $type !== 'array' ){ + trigger_error('$section_data is ' . $type . '. Array expected'); + return; + } + + $section_data += ['attributes' => [], 'type' => 'text']; + $section_data['attributes'] += ['class' => '']; + $orig_attrs = $section_data['attributes']; + $section_data['attributes']['data-gp-section'] = $curr_section_num; + $section_types = \gp\tool\Output\Sections::GetTypes(); + + if( \gp\tool\Output::ShowEditLink() && \gp\admin\Tools::CanEdit($this->gp_index) ){ + + if( isset($section_types[$section_data['type']]) ){ + $title_attr = $section_types[$section_data['type']]['label']; + }else{ + $title_attr = sprintf($langmessage['Section %s'], $curr_section_num+1); + } + + $attrs = [ + 'title' => $title_attr, + 'data-cmd' => 'inline_edit_generic', + 'data-arg' => $section_data['type'] . '_inline_edit', + ]; + $link = \gp\tool\Output::EditAreaLink( + $edit_index, + $this->title, + $langmessage['edit'], + 'section='.$curr_section_num,$attrs + ); + + $section_data['attributes']['data-gp-area-id'] = $edit_index; + + //included page target + $include_link = self::IncludeLink($section_data); + + + //section control links + if( $section_data['type'] != 'wrapper_section' ){ + ob_start(); + echo ''; + echo $link; + echo $include_link; + echo \gp\tool::Link( + $this->title, + $langmessage['Manage Sections'], + 'cmd=ManageSections', + [ + 'class' => 'manage_sections', + 'data-cmd' => 'inline_edit_generic', + 'data-arg' => 'manage_sections', + ] + ); + echo ''; + echo \gp\tool::Link( + $this->title, + $langmessage['rename/details'], + 'cmd=renameform&index=' . urlencode($this->gp_index), + ['data-cmd' => 'gpajax'] + ); + echo \gp\tool::Link( + '/Admin/Revisions/'.$this->gp_index, + $langmessage['Revision History'], + '' + ); + echo ''; + echo \gp\tool::Link('Admin/Menu', $langmessage['file_manager']); + echo ''; + \gp\tool\Output::$editlinks .= ob_get_clean(); + } + + $section_data['attributes']['id'] = 'ExtraEditArea' . $edit_index; + } + + $content .= $this->SectionNode($section_data, $orig_attrs); + + if( $section_data['type'] == 'wrapper_section' ){ + + for( $cc=0; $cc < $section_data['contains_sections']; $cc++ ){ + $content .= $this->GetSection($section_num); + } + + }else{ + \gp\tool\Output::$nested_edit = true; + $content .= \gp\tool\Output\Sections::RenderSection($section_data, $curr_section_num, $this->title, $this->file_stats); + \gp\tool\Output::$nested_edit = false; + } + + if( !isset($section_data['nodeName']) ){ + $content .= '
    '; + $content .= '
    '; + }else{ + $content .= \gp\tool\Output\Sections::EndTag($section_data['nodeName']); + } + + return $content; + } + + + /** + * Return a link to the included page or extra area + * + * @return string + * + */ + public static function IncludeLink($section_data){ + global $langmessage; + + if( $section_data['type'] != 'include' || !array_key_exists('include_type',$section_data) ){ + return ''; + } + + if( isset($section_data['index']) ){ + return \gp\tool::Link($section_data['content'], $langmessage['view/edit_page']); + } + + if( $section_data['include_type'] == 'extra' ){ + + // get the real section type of the included extra area + $extra_sections = \gp\tool\Output\Extra::ExtraContent($section_data['content']); + $extra_section_type = $extra_sections[0]['type']; + if($extra_section_type != 'text' ){ + // we can only edit text sections on Admin/Extra + return ''; + } + + return \gp\tool::Link( + 'Admin/Extra/' . rawurlencode($section_data['content']), + $langmessage['edit'] . ' » ' . str_replace('_', ' ', htmlspecialchars($section_data['content'])), // $langmessage['theme_content'] + 'cmd=EditExtra' + ); + } + + return ''; + } + + + public function GetSectionForClipboard(&$section_num){ + global $langmessage; + + if( !isset($this->file_sections[$section_num]) ){ + trigger_error('invalid section number'); + return; + } + + $curr_section_num = $section_num; + $section_num++; + + $content = ''; + $section_data = $this->file_sections[$curr_section_num]; + + //make sure section_data is an array + $type = gettype($section_data); + if( $type !== 'array' ){ + trigger_error('$section_data is ' . $type . '. Array expected'); + return; + } + + $section_data += ['attributes' => [], 'type' => 'text']; + $section_data['attributes'] += ['class' => '']; + // $section_data['attributes']['gp_type'] = $section_data['type']; + $section_data['gp_hidden'] = false; + $orig_attrs = $section_data['attributes']; + + $content .= $this->SectionNode($section_data, $orig_attrs); + + if( $section_data['type'] == 'wrapper_section' ){ + + for( $cc=0; $cc < $section_data['contains_sections']; $cc++ ){ + $content .= $this->GetSectionForClipboard($section_num); + } + + }else{ + \gp\tool\Output::$nested_edit = true; + $content .= \gp\tool\Output\Sections::RenderSection($section_data, $curr_section_num, $this->title, $this->file_stats); + \gp\tool\Output::$nested_edit = false; + } + + if( !isset($section_data['nodeName']) ){ + $content .= '
    '; + }else{ + $content .= \gp\tool\Output\Sections::EndTag($section_data['nodeName']); + } + + return $content; + } + + + public function GalleryImages(){ + + if( isset($_GET['dir']) ){ + $dir_piece = $_GET['dir']; + }elseif( isset($this->meta_data['gallery_dir']) ){ + $dir_piece = $this->meta_data['gallery_dir']; + }else{ + $dir_piece = '/image'; + } + + //remember browse directory + $this->meta_data['gallery_dir'] = $dir_piece; + $this->SaveThis(false); + + \gp\admin\Content\Uploaded::InlineList($dir_piece); + } + + + /** + * Used by slideshow addons + * @deprecated 3.6rc4 + * + */ + public function SaveSection_Text($section){ + return \gp\tool\Editing::SectionFromPost_Text($this->file_sections[$section]); + } + +} diff --git a/include/Page/Layout.php b/include/Page/Layout.php new file mode 100644 index 0000000..009a06c --- /dev/null +++ b/include/Page/Layout.php @@ -0,0 +1,310 @@ +from_page = true; + } + $query_string .= '&'; + $query_string = ltrim($query_string,'&'); + + switch($cmd){ + case 'layout': + $this->SelectLayout($url,$query_string); + $this->show_popup = true; + return; + case 'uselayout': + $this->SetLayout(); + return; + case 'restorelayout': + $this->RestoreLayout(); + return; + } + } + + public function Result(){ + global $page; + + if( $this->from_page && $this->title){ + if( !$this->show_popup ){ + $url = \gp\tool::AbsoluteUrl($this->title,'',true,false,true); + $page->ajaxReplace[] = array('location',$url,0); + } + return true; + } + + return $this->show_popup; + } + + /** + * Remove any layout setting from a page. + * The page will revert to inheriting the layout setting from the site configuration or a parent page + * + */ + public function RestoreLayout(){ + global $gp_titles,$gp_index,$langmessage; + + $index = $_POST['index']; + $title = \gp\tool::IndexToTitle($index); + + if( !$title ){ + msg($langmessage['OOPS']); + return; + } + $this->title = $title; + + if( !\gp\tool\Nonce::Verify('restore') ){ + msg($langmessage['OOPS']); + return; + } + + + unset($gp_titles[$index]['gpLayout']); + return \gp\admin\Tools::SavePagesPHP(true, true); + } + + + /** + * Assign a layout to the $title. Child pages without a layout assigned will inherit this setting + * @param string $title + */ + public function SetLayout(){ + global $gp_index, $gp_titles, $langmessage, $gpLayouts; + + $index = $_POST['index']; + $title = \gp\tool::IndexToTitle($index); + + if( !$title ){ + msg($langmessage['OOPS']); + return; + } + $this->title = $title; + + $layout = $_POST['layout']; + if( !isset($gpLayouts[$layout]) ){ + msg($langmessage['OOPS']); + return; + } + + if( !\gp\tool\Nonce::Verify('use_'.$layout) ){ + msg($langmessage['OOPS']); + return; + } + + + //unset, then reset if needed + unset($gp_titles[$index]['gpLayout']); + $currentLayout = \gp\Page::OrConfig($index,'gpLayout'); + if( $currentLayout != $layout ){ + $gp_titles[$index]['gpLayout'] = $layout; + } + + return \gp\admin\Tools::SavePagesPHP(true, true); + } + + + /** + * Display current layout, list of available layouts and list of titles affected by the layout setting for $title + * + */ + public function SelectLayout($url,$query_string){ + global $gp_titles, $gpLayouts, $langmessage, $config, $gp_index; + + $index = $_REQUEST['index']; + $title = \gp\tool::IndexToTitle($index); + if( !$title ){ + echo $langmessage['OOPS']; + return; + } + + $this->title = $title; + + $Inherit_Info = \gp\admin\Menu\Tools::Inheritance_Info(); + $curr_layout = \gp\admin\Menu\Tools::CurrentLayout($index); + $curr_info = $gpLayouts[$curr_layout]; + + + echo '
    '; + + echo '

    '; + echo $langmessage['current_layout'].':   '; + echo '   '; + echo str_replace('_',' ',$curr_info['label']); + echo '

    '; + + if( !empty($gp_titles[$index]['gpLayout']) ){ + echo '

    '; + + if( isset($Inherit_Info[$index]['parent_layout']) ){ + $parent_layout = $Inherit_Info[$index]['parent_layout']; + }else{ + $parent_layout = $config['gpLayout']; + } + $parent_info = $gpLayouts[$parent_layout]; + + echo $langmessage['restore'].': '; + $span = ' '; + echo \gp\tool::Link($url,$span.$parent_info['label'],$query_string.'cmd=restorelayout&index='.urlencode($index),array('data-cmd'=>'postlink','title'=>$langmessage['restore']),'restore'); + echo '

    '; + } + + + echo ''; + + echo ''; + + if( count($gpLayouts) < 2 ){ + echo ''; + echo '
    '; + echo $langmessage['available_layouts']; + echo ''; + echo $langmessage['theme']; + echo '
    '; + echo $langmessage['Empty']; + echo '
    '; + echo \gp\tool::Link('Admin_Theme_Content',$langmessage['new_layout']); + echo '
    '; + return; + } + + foreach($gpLayouts as $layout => $info){ + if( $layout == $curr_layout ){ + continue; + } + echo ''; + echo ''; + echo ' '; + if( $layout != $curr_layout ){ + echo \gp\tool::Link($url,$info['label'],$query_string.'cmd=uselayout&index='.urlencode($index).'&layout='.urlencode($layout),array('data-cmd'=>'postlink'),'use_'.$layout); + + } + echo ''; + echo $info['theme']; + echo ''; + + } + echo ''; + + + //show affected pages + $affected = self::GetAffectedFiles($index); + + echo '
    '; + + echo ''; + echo '
    '.$langmessage['affected_files'].'
    '; + + echo '

    '.$langmessage['about_layout_change'].'

    '; + echo '

    '; + + $label = \gp\tool::GetLabelIndex($index,false); + echo \gp\tool::LabelSpecialChars($label); + + $i = 0; + foreach($affected as $affected_label){ + $i++; + echo ', '.$affected_label; + } + echo '

    '; + + echo '

    '; + echo ' '; + echo '

    '; + + echo '

    '; + echo ''.$langmessage['see_also'].' '; + echo \gp\tool::Link('Admin_Theme_Content',$langmessage['layouts']); + echo '

    '; + + echo ''; + } + + /** + * Get a list of titles that inherit layout settings from the page with $index + * @param string $index + * + */ + public function GetAffectedFiles($index){ + global $gp_titles, $gp_menu; + + $temp = $gp_menu; + reset($temp); + $result = array(); + + $i = 0; + do{ + $menu_key = key($temp); + $info = current($temp); + if( !isset($info['level']) ){ + break; + } + $level = $info['level']; + + unset($temp[$menu_key]); + if( $index === $menu_key ){ + self::InheritingLayout($level+1,$temp,$result); + } + $i++; + }while( (count($temp) > 0) ); + return $result; + } + + public function InheritingLayout($searchLevel,&$menu,&$result){ + global $gp_titles; + + $children = true; + do{ + $menu_key = key($menu); + $info = current($menu); + if( !isset($info['level']) ){ + break; + } + $level = $info['level']; + + if( $level < $searchLevel ){ + return; + } + if( $level > $searchLevel ){ + if( $children ){ + self::InheritingLayout($level,$menu,$result); + }else{ + unset($menu[$menu_key]); + } + continue; + } + + unset($menu[$menu_key]); + if( !empty($gp_titles[$menu_key]['gpLayout']) ){ + $children = false; + continue; + } + $children = true; + + //exclude external links + if( $menu_key[0] == '_' ){ + continue; + } + + $label = \gp\tool::GetLabelIndex($menu_key,false); + $result[] = \gp\tool::LabelSpecialChars($label); + }while( count($menu) > 0 ); + + } + + + +} diff --git a/include/Page/Rename.php b/include/Page/Rename.php new file mode 100644 index 0000000..8eb6d12 --- /dev/null +++ b/include/Page/Rename.php @@ -0,0 +1,392 @@ +requested); + $label = \gp\tool::GetLabelIndex($index); + $title = \gp\tool::IndexToTitle($index); + $title_info = $gp_titles[$index]; + + $title_info += array( + 'browser_title' => '', + 'keywords' => '', + 'description' => '', + 'rel' => '', + ); + + if( empty($_REQUEST['new_title']) ){ + $new_title = \gp\tool::LabelSpecialChars($label); + }else{ + $new_title = htmlspecialchars($_REQUEST['new_title']); + } + $new_title = str_replace('_',' ',$new_title); + + + ob_start(); + echo '
    '; + echo '
    '; + + echo ''; + echo ''; + + + echo '

    '.$langmessage['rename/details'].'

    '; + + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + //label + self::FormLabel('label'); + echo ''; + echo ''; + + + //slug (title) + $attr = ''; + $class = 'new_title'; + + if( $title == \gp\admin\Tools::LabelToSlug($label) ){ + //$attr = 'disabled="disabled" '; + $attr = 'readonly="readonly"" '; + $class .= ' sync_label'; + } + self::FormLabel('Slug/URL'); + echo ''; + self::ToggleSync($attr); + echo ''; + + + + //browser title defaults to label + $attr = ''; + $class = 'browser_title'; + $browser_title = $title_info['browser_title']; + self::FormLabel('browser_title',$title_info['browser_title']); + + if( empty($title_info['browser_title']) ){ + $browser_title = htmlspecialchars($label); + $attr = 'readonly="readonly" '; + $class .= ' sync_label'; + } + + echo ''; + self::ToggleSync($attr); + echo ''; + + + //meta keywords + self::FormLabel('keywords',$title_info['keywords']); + echo ''; + echo ''; + + + //meta description + self::FormLabel('description',$title_info['description']); + $count_label = sprintf($langmessage['_characters'],''.strlen($title_info['description']).''); + echo ''; + echo ''; + echo ''.$count_label.''; + echo ''; + echo ''; + + + //robots + self::FormLabel('robots',$title_info['rel']); + echo ''; + + echo ''; + + echo ''; + + echo ''; + echo '
    '; + echo $langmessage['options']; + echo '
    '; + + + //redirection + echo '

    '; + echo ''; + echo '

    '; + + echo '

    '; + if( self::$hidden_rows ) echo '   + '.$langmessage['more_options'].''; + echo '

    '; + + echo '

    '; + echo ' '; + echo ''; + echo ''; + echo '

    '; + + echo '
    '; + echo '
    '; + + $content = ob_get_clean(); + + $page->ajaxReplace = array(); + + + $array = array(); + $array[0] = 'gpabox'; + $array[1] = ''; + $array[2] = $content; + $page->ajaxReplace[] = $array; + + + + //call renameprep function after admin_box + $array = array(); + $array[0] = 'renameprep'; + $array[1] = ''; + $array[2] = ''; + $page->ajaxReplace[] = $array; + } + + protected static function FormLabel($lang_key, $hidden_if_empty = 'not-empty' ){ + global $langmessage; + + if( empty($hidden_if_empty) ){ + echo ''; + self::$hidden_rows = true; + }else{ + echo ''; + } + echo ''; + echo $langmessage[$lang_key]; + echo ''; + } + + + /** + * Display Sync Toggle + * + */ + protected static function ToggleSync($attr){ + global $langmessage; + + echo '
    '; + if( empty( $attr ) ){ + echo ''.$langmessage['sync_with_label'].''; + echo ''.$langmessage['edit'].''; + }else{ + echo ''.$langmessage['sync_with_label'].''; + echo ''.$langmessage['edit'].''; + } + echo '
    '; + } + + + /** + * Handle renaming a page based on POSTed data + * + */ + public static function RenameFile($title){ + global $langmessage, $page, $gp_index, $gp_titles; + + $page->ajaxReplace = array(); + + + //change the title + $title = self::RenameFileWorker($title); + if( $title === false ){ + return false; + } + + + if( !isset($gp_index[$title]) ){ + msg($langmessage['OOPS']); + return false; + } + + $id = $gp_index[$title]; + $title_info = &$gp_titles[$id]; + + //change the label + $title_info['label'] = \gp\admin\Tools::PostedLabel($_POST['new_label']); + if( isset($title_info['lang_index']) ){ + unset($title_info['lang_index']); + } + + //browser_title, keywords, description + self::SetInfo($title_info, 'browser_title'); + self::SetInfo($title_info, 'keywords'); + self::SetInfo($title_info, 'description'); + self::SetRobots($title_info); + + + //same as auto-generated? + $auto_browser_title = strip_tags($title_info['label']); + if( isset($title_info['browser_title']) && $title_info['browser_title'] == $auto_browser_title ){ + unset($title_info['browser_title']); + } + + if( !\gp\admin\Tools::SavePagesPHP(true,true) ){ + return false; + } + + return $title; + } + + + /** + * Set the title_info value if not emptpy + * Otherwise, unset the key + * + */ + private static function SetInfo( &$title_info, $key){ + + if( isset($_POST[$key]) ){ + $title_info[$key] = htmlspecialchars($_POST[$key]); + if( empty($title_info[$key]) ){ + unset($title_info[$key]); + } + } + } + + /** + * Set the robot visibility + * + */ + private static function SetRobots(&$title_info){ + + $title_info['rel'] = ''; + if( isset($_POST['nofollow']) ){ + $title_info['rel'] = 'nofollow'; + } + + if( isset($_POST['noindex']) ){ + $title_info['rel'] .= ',noindex'; + } + + $title_info['rel'] = trim($title_info['rel'],','); + if( empty($title_info['rel']) ){ + unset($title_info['rel']); + } + } + + + private static function RenameFileWorker($title){ + global $langmessage,$dataDir,$gp_index; + + //use new_label or new_title + if( isset($_POST['new_title']) ){ + $new_title = \gp\admin\Tools::PostedSlug($_POST['new_title']); + }else{ + $new_title = \gp\admin\Tools::LabelToSlug($_POST['new_label']); + } + + //title unchanged + if( $new_title == $title ){ + return $title; + } + + $special_file = false; + if( \gp\tool::SpecialOrAdmin($title) !== false ){ + $special_file = true; + } + + if( !\gp\admin\Tools::CheckTitle($new_title,$message) ){ + msg($message); + return false; + } + + $old_gp_index = $gp_index; + + //re-index: make the new title point to the same data index + $old_file = \gp\tool\Files::PageFile($title); + $file_index = $gp_index[$title]; + unset($gp_index[$title]); + $gp_index[$new_title] = $file_index; + + + //rename the php file + if( !$special_file ){ + $new_file = \gp\tool\Files::PageFile($new_title); + + //if the file being renamed doesn't use the index naming convention, then we'll still need to rename it + if( $new_file != $old_file ){ + $new_dir = dirname($new_file); + $old_dir = dirname($old_file); + if( !\gp\tool\Files::Rename($old_dir,$new_dir) ){ + msg($langmessage['OOPS'].' (N3)'); + $gp_index = $old_gp_index; + return false; + } + } + + //gallery rename + \gp\special\Galleries::RenamedGallery($title,$new_title); + } + + + //create a 301 redirect + if( isset($_POST['add_redirect']) && $_POST['add_redirect'] == 'add' ){ + \gp\admin\Settings\Missing::AddRedirect($title,$new_title); + } + + + \gp\tool\Plugins::Action('RenameFileDone',array($file_index, $title, $new_title)); + + return $new_title; + } + + /** + * Rename a page + * + */ + public static function RenamePage(){ + global $langmessage, $gp_index, $page; + + $new_title = self::RenameFile($page->title); + if( ($new_title !== false) && $new_title != $page->title ){ + msg(sprintf($langmessage['will_redirect'],\gp\tool::Link_Page($new_title))); + + $page->head .= ''; + $page->ajaxReplace[] = array('location',\gp\tool::GetUrl($new_title,'',false,''),15000); + return true; + } + return false; + } + + } + +} + +namespace{ + class gp_rename extends \gp\Page\Rename{} +} diff --git a/include/Page/Visibility.php b/include/Page/Visibility.php new file mode 100644 index 0000000..b7b143a --- /dev/null +++ b/include/Page/Visibility.php @@ -0,0 +1,47 @@ +''); + + self::Toggle($page->gp_index, $_REQUEST['visibility']); + + $page->visibility = null; + if( isset($gp_titles[$page->gp_index]['vis']) ){ + $page->visibility = $gp_titles[$page->gp_index]['vis']; + } + } + +} diff --git a/include/admin/About.php b/include/admin/About.php new file mode 100644 index 0000000..91c410b --- /dev/null +++ b/include/admin/About.php @@ -0,0 +1,82 @@ +'; + + echo '

    '.\CMS_NAME.' CMS

    '; + + echo '
    '; + + + echo '

    You\'re currently using version '.\gpversion.' of our free, open source and easy to use content management system.'; + echo ' Our code is hosted on GitHub '; + echo 'and licensed under version 2 of the GNU General Public License. '; + echo '

    '; + + echo '
    '; + + + + echo '

    Thanks For Using '.\CMS_NAME.'

    '; + echo '
    '; + + echo '

    '; + echo 'We\'ve worked very hard to find a balance between the ease of use and functionality in Content Management Systems and we think we\'ve done a pretty good job. '; + echo ' You may agree or disagree though, and the only way for us to know is to hear from you. '; + echo ' We want to know what you think. Here\'s how:'; + echo '

    '; + + echo 'Does '.\CMS_NAME.' Work?'; + echo '

    Obviously the first step is to get '.\CMS_NAME.' working correctly.'; + echo ' If it\'s not working for you and you think it\'s because of a bug, you can report it and we\'ll work on fixing it.'; + echo '

    '; + + echo 'Does '.\CMS_NAME.' Work Well?'; + echo '

    This one is a bit more subjective, but just as important. '; + echo ' There are multiple ways to give us feedback. The following services allow you to rate and comment on '.\CMS_NAME.'. '; + echo '

    '; + echo '

    Fork on GitHub

    '; + echo '

    OpensourceCMS.com

    '; + + + $projects = []; + $projects['ckEditor'] = 'ckeditor.com/ckeditor-4'; + $projects['elFinder'] = 'studio-42.github.io/elFinder'; + $projects['ColorBox'] = 'www.jacklmoore.com/colorbox'; + $projects['Bootstrap'] = 'getbootstrap.com'; + $projects['Bootswatch'] = 'bootswatch.com'; + $projects['jQuery'] = 'jquery.com'; + $projects['jQuery UI'] = 'jqueryui.com'; + $projects['ScssPhp'] = 'github.com/leafo/scssphp'; + $projects['PHPMailer'] = 'github.com/PHPMailer/PHPMailer'; + + + echo '
    '; + echo '

    Our Thanks

    '; + echo '
    '; + echo '

    '.\CMS_NAME.' would not have been possible if it wasn\'t for the prosperous open source community and rich selection of successful open source projects. '; + echo ' We have benefited tremendously from the community and have borrowed ideas as well as integrated other freely available code. '; + echo ' Here are some of the projects we have benefited the most from. '; + echo '

    '; + + echo ''; + foreach($projects as $name => $url){ + echo ''; + } + echo '
    ProjectWebsite
    '; + echo $name; + echo ''; + echo ''.$url.''; + echo '
    '; + + echo ''; + } + +} diff --git a/include/admin/Addon/Available.php b/include/admin/Addon/Available.php new file mode 100644 index 0000000..02e1f66 --- /dev/null +++ b/include/admin/Addon/Available.php @@ -0,0 +1,82 @@ +ShowHeader(); + + echo '
    '; + + if( count($this->avail_addons) == 0 ){ + //echo ' -empty- '; + }else{ + echo ''; + echo ''; + + $avail_addons = $this->avail_addons; + + // sort available addons by name + uasort($avail_addons, function($a, $b) { + return strnatcmp($a['Addon_Name'], $b['Addon_Name']); + }); + + $i=0; + foreach($avail_addons as $folder => $info ){ + + if( $info['upgrade_key'] ){ + continue; + } + + $info += array('About' => ''); + + echo ''; + $i++; + } + echo '
    '; + echo $langmessage['name']; + echo ''; + echo $langmessage['version']; + echo ''; + echo $langmessage['options']; + echo ''; + echo $langmessage['description']; + echo '
    '; + echo str_replace(' ', ' ', $info['Addon_Name']); + echo '
    /addons/' . $folder . ''; + echo '
    '; + echo $info['Addon_Version']; + echo ''; + echo \gp\tool::Link( + 'Admin/Addons', + $langmessage['Install'], + 'cmd=LocalInstall&source=' . $folder, + array('data-cmd' => 'cnreq') + ); + echo ''; + echo $info['About']; + if( isset($info['Addon_Unique_ID']) && is_numeric($info['Addon_Unique_ID']) ){ + echo '
    '; + echo $this->DetailLink('plugins', $info['Addon_Unique_ID'], 'More Info...'); + } + echo '
    '; + + } + + $this->InvalidFolders(); + $this->Instructions(); + + } + +} diff --git a/include/admin/Addon/Install.php b/include/admin/Addon/Install.php new file mode 100644 index 0000000..2f3e13e --- /dev/null +++ b/include/admin/Addon/Install.php @@ -0,0 +1,271 @@ +page ){ + $this->page->css_admin[] = '/include/css/addons.css'; + $this->page->head_js[] = '/include/js/rate.js'; + $this->page->head_js[] = '/include/js/auto_width.js'; + } + } + + /** + * Output addon heading + * + */ + public function ShowHeader( $addon_name = false ){ + global $langmessage; + + //build links + $header_paths = array(); + $header_paths[$this->scriptUrl] = $langmessage['manage']; + $header_paths[$this->scriptUrl.'/Available'] = $langmessage['Available']; + + + if( $this->config_index == 'themes' ){ + $root_label = $langmessage['themes']; + if( gp_remote_themes ){ + $this->FindForm(); + $header_paths[$this->scriptUrl.'/Remote'] = $langmessage['Search']; + } + + }else{ + $root_label = $langmessage['plugins']; + if( gp_remote_plugins ){ + $this->FindForm(); + $header_paths[$this->scriptUrl.'/Remote'] = $langmessage['Search']; + } + } + + if( $addon_name ){ + $header_paths = array(); + $header_paths[$this->scriptUrl] = $langmessage['manage']; + $header_paths[$this->page->requested] = $addon_name; + } + + + $list = array(); + foreach($header_paths as $slug => $label){ + + if( $this->page->requested == $slug ){ + $list[] = ''.$label.''; + }else{ + $list[] = \gp\tool::Link($slug,$label); + } + } + + + echo '

    '; + echo $root_label; + echo ' »'; + + echo implode('', $list ); + + echo '

    '; + + } + + + /** + * Remote Install Functions + * + */ + public function RemoteInstall(){ + global $langmessage; + + echo '

    '.$langmessage['Installation'].'

    '; + + $name = ''.htmlspecialchars($_REQUEST['name']).''; + echo '

    '.$langmessage['Addon_Install_Warning'].'

    '; + echo '

    '.sprintf($langmessage['Selected_Install'],$name,\CMS_READABLE_DOMAIN).'

    '; + + $_REQUEST += array('order'=>''); + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + + echo ''; + echo '
    '; + } + + public function RemoteInstallConfirmed($type = 'plugin'){ + + $_POST += array('order'=>''); + + + $installer = new \gp\admin\Addon\Installer(); + + $installer->code_folder_name = $this->code_folder_name; + $installer->config_index = $this->config_index; + + $installer->InstallRemote( $type, $_POST['id'], $_POST['order'] ); + $installer->OutputMessages(); + + return $installer; + } + + + public function SearchOrder(){ + + if( isset($_REQUEST['order']) && isset($this->searchOrderOptions[$_REQUEST['order']]) ){ + $this->searchOrder = $_REQUEST['order']; + $this->searchQuery .= '&order='.rawurlencode($_REQUEST['order']); + }else{ + reset($this->searchOrderOptions); + $this->searchOrder = key($this->searchOrderOptions); + } + + } + + /** + * Display available search options + * + */ + public function SearchOptions( $nav_on_top = true ){ + echo '
    '; + + if( $nav_on_top ){ + $this->SearchNavLinks(); + } + + echo '
    '; + foreach($this->searchOrderOptions as $key => $label){ + if( $key === $this->searchOrder ){ + echo ''.$label.''; + }else{ + echo \gp\tool::Link($this->searchUrl,$label,$this->searchQuery.'&order='.$key); + } + } + echo '
    '; + + if( !$nav_on_top ){ + $this->SearchNavLinks(); + } + + echo '
    '; + } + + + public function FindForm(){ + global $langmessage; + + $_GET += array('q'=>''); + + echo '
    '; + echo '
    '; + echo ' '; + echo ''; + echo '
    '; + echo '
    '; + } + + public function InstallLink($row){ + global $config,$langmessage; + + $installed = in_array($row['id'],$this->installed_ids); + + if( !$installed && ($row['price_unit'] > 0) ){ + $label = ' Install For $'.$row['price_unit']; + echo self::DetailLink($row['type'], $row['id'], $label, '&cmd=install_info'); + return; + } + + if( $installed ){ + $label = $langmessage['Update Now']; + }else{ + $label = $langmessage['Install Now']; + } + + if( $row['type'] == 'theme' ){ + $url = 'Admin_Theme_Content'; + }else{ + $url = 'Admin/Addons'; + } + + $link = 'cmd=RemoteInstall'; + $link .= '&name='.rawurlencode($row['name']); + $link .= '&type='.rawurlencode($row['type']); + $link .= '&id='.rawurlencode($row['id']); + + echo \gp\tool::Link($url,$label,$link); + } + + public function SearchNavLinks(){ + + $pages = ceil($this->searchMax/$this->searchPerPage); + + echo '
    '; + \gp\special\Search::PaginationLinks( $this->searchPage, $pages, $this->searchUrl, $this->searchQuery, 'page'); + echo '
    '; + } + + + /** + * Show folders in /addons that didn't make it into the available list + * + */ + public function InvalidFolders(){ + global $langmessage; + + if( empty($this->invalid_folders) ){ + return; + } + + echo '
    '; + echo '

    Invalid Addon Folders

    '; + echo ''; + echo ''; + foreach($this->invalid_folders as $folder => $msg){ + + if( isset($this->avail_addons[$folder]) ){ + continue; //skip false positives + } + + echo ''; + } + echo '
    '; + echo $langmessage['name']; + echo ' 
    '; + echo htmlspecialchars($folder); + echo ''; + echo htmlspecialchars($msg); + echo '
    '; + + } + +} diff --git a/include/admin/Addon/Installer.php b/include/admin/Addon/Installer.php new file mode 100644 index 0000000..d5a11af --- /dev/null +++ b/include/admin/Addon/Installer.php @@ -0,0 +1,1399 @@ +source should already be set + * + */ + public function Install(){ + global $langmessage; + + $success = $this->InstallSteps(); + + if( !$this->remote_install && !is_dir($this->source) ){ + $this->message($langmessage['OOPS'].' (Source not found)'); + return false; + } + + if( $success ){ + $this->message( sprintf($langmessage['installed'], $this->display_name ) ); + }else{ + $this->Failed(); + } + + $this->CleanInstallFolder(); + + return $success; + } + + + + /** + * Get and install addon from a remote source + * @param string $type Type of addon (plugin or theme) + * @param int $id Addon id + * @param int $order Purchase order id + * + */ + public function InstallRemote( $type, $id, $order = null ){ + global $langmessage; + + + // check values + if( empty($type) ){ + $this->message($langmessage['OOPS'].' - Invalid Request (type)'); + return false; + } + + if( empty($id) || !ctype_digit($id) ){ + $this->message($langmessage['OOPS'].' - Invalid Request (id)'); + return false; + } + + + $this->remote_install = true; + $this->type = $type; + $this->id = (int)$id; + + if( !is_null($order) ){ + $this->order = (int)$order; + } + + return $this->Install(); + } + + public function OutputMessages(){ + foreach($this->messages as $msg){ + msg($msg); + } + } + + + /** + * Remove an addon from the site configuration + * Delete code folders if needed + * + * @return bool + */ + public function Uninstall( $addon ){ + global $config, $langmessage, $gp_titles, $gp_menu, $gp_index; + + $this->GetAddonData(); + + $addon_config = \gp\tool\Plugins::GetAddonConfig($addon); + if( !$addon_config ){ + $this->message($langmessage['OOPS'].' (Already uninstalled)'); + return false; + } + + unset($config['addons'][$addon]); + + + //remove links + $installedGadgets = $this->GetInstalledComponents($config['gadgets'],$addon); + $this->RemoveFromHandlers($installedGadgets); + + + //remove from gp_index, gp_menu + $installedLinks = $this->GetInstalledComponents($gp_titles,$addon); + foreach($installedLinks as $index){ + if( isset($gp_menu[$index]) ){ + unset($gp_menu[$index]); + } + $title = \gp\tool::IndexToTitle($index); + if( $title ){ + unset($gp_index[$title]); + } + } + + $this->RemoveFromConfig($config['gadgets'],$addon); + $this->RemoveFromConfig($config['admin_links'],$addon); + $this->RemoveFromConfig($gp_titles,$addon); + $this->CleanHooks($addon); + + if( !\gp\admin\Tools::SaveAllConfig() ){ + $this->message($langmessage['OOPS']); + return false; + } + + $this->RemoveFolders($addon_config); + + //Record the history + $this->addonHistory[] = $this->UninstallHistory($addon_config); + $this->SaveAddonData(); + + + if( $addon_config['order'] ){ + $img_path = \gp\tool::IdUrl('ci'); + \gp\tool::IdReq($img_path); + } + + + $this->message($langmessage['SAVED']); + return true; + } + + + /** + * Delete the code & data folders for an addon + * @param array $addon_config + */ + private function RemoveFolders($addon_config){ + + if( !$this->rm_folders ){ + return; + } + + //only delete code if remote installation + if( isset($addon_config['remote_install']) && $addon_config['remote_install'] ){ + + $installFolder = $addon_config['code_folder_full']; + if( file_exists($installFolder) ){ + \gp\tool\Files::RmAll($installFolder); + } + + } + + //only delete data if data_folder is not empty + if( !empty($addon_config['data_folder']) ){ + $dataFolder = $addon_config['data_folder_full']; + if( file_exists($dataFolder) ){ + \gp\tool\Files::RmAll($dataFolder); + } + } + } + + + /** + * Prepare a history record for the addon history + * @param array $addon_config + */ + public function UninstallHistory($addon_config){ + + $history = array(); + $history['name'] = $addon_config['name']; + $history['action'] = 'uninstalled'; + $history['time'] = time(); + + if( isset($addon_config['id']) ){ + $history['id'] = $addon_config['id']; + } + + return $history; + } + + + /** + * Run through the installation process + * + */ + public function InstallSteps(){ + global $dataDir, $langmessage; + + $this->GetAddonData(); // addonHistory + $this->Init_PT(); // $this->config + + + //get from remote + if( $this->remote_install && !$this->GetRemote() ){ + return false; + } + + //check ini contents + if( !$this->CheckIni() ){ + return false; + } + + $this->SetDestination(); + + $this->DataFolder(); + $this->IniContents(); + + if( !$this->PrepConfig() ){ + return false; + } + + if( !$this->CheckFile() ){ + return false; + } + + //hooks + if( !$this->Hooks() ){ + return false; + } + + //layout + if( !$this->Layout() ){ + return false; + } + + //move new addon folder into place + if( !$this->FinalizeFolder() ){ + return false; + } + + if( !$this->FinalizeConfig() ){ + return false; + } + + + // Save + if( !\gp\admin\Tools::SaveAllConfig() ){ + $this->message($langmessage['OOPS'].' (Configuration not saved)'); + return false; + } + + if( !is_null($this->order) ){ + $img_path = \gp\tool::IdUrl('ci'); + \gp\tool::IdReq($img_path); + } + + $this->UpdateHistory(); + + return true; + + } + + + /** + * Prepare $this->config and make sure $this->addon_folder exists + * + */ + public function Init_PT(){ + global $config, $dataDir, $gpLayouts; + + if( !isset($config[$this->config_index]) ){ + $config[$this->config_index] = array(); + } + + $this->config =& $config[$this->config_index]; + $this->config_cache = $config; + $this->layouts_cache = $gpLayouts; + + + if( !$this->addon_folder_rel ){ + if( $this->remote_install ){ + $this->addon_folder_rel = '/data/'.$this->code_folder_name; + }else{ + $this->addon_folder_rel = '/'.basename( dirname($this->source) ); + } + } + $this->addon_folder = $dataDir.$this->addon_folder_rel; + + \gp\tool\Files::CheckDir($this->addon_folder); + } + + + /** + * Check the Ini contents + * + */ + protected function CheckIni(){ + + $this->display_name = basename($this->source); + if( !$this->GetINI($this->source,$error) ){ + + //local themes don't need addon.ini files + if( empty($this->new_layout) ){ + $this->message( $error ); + return false; + } + } + + return true; + } + + + /** + * Set the install destination + * + */ + protected function SetDestination(){ + + $this->config_key = \gp\admin\Addon\Tools::UpgradePath($this->ini_contents,$this->config_index); + $this->upgrade_key = $this->config_key; + + if( $this->remote_install ){ + if( $this->config_key ){ + $this->dest = $this->addon_folder.'/'.$this->config_key; + }else{ + $this->dest = $this->TempFile(); + } + }else{ + $this->dest = $this->source; + } + $this->dest_name = basename($this->dest); + + if( !$this->config_key ){ + $this->config_key = $this->dest_name; + } + } + + + /** + * The data folder will not always be the same as the addon folder + * + */ + protected function DataFolder(){ + global $dataDir; + + if( !empty($this->config[$this->config_key]['data_folder']) ){ + $this->data_folder = $this->config[$this->config_key]['data_folder']; + }elseif( !empty($this->upgrade_key) && file_exists( $dataDir.'/data/_addondata/'.$this->upgrade_key) ){ + $this->data_folder = $this->upgrade_key; + }else{ + $this->data_folder = $this->dest_name; + } + } + + + /** + * Prepare the configuration array for installation + * + */ + public function PrepConfig(){ + + if( !$this->has_hooks ){ + return true; + } + + + //make sure we have an array + if( !isset($this->config[$this->config_key]) ){ + $this->config[$this->config_key] = array(); + }elseif( !is_array($this->config[$this->config_key]) ){ + $this->message('$this->config[addon] is not an array'); + return false; + } + + return true; + } + + + /** + * Get the Ini contents and check values + * @return bool + * + */ + public function GetINI($ini_dir,&$error){ + global $langmessage; + + $error = false; + $ini_file = $ini_dir.'/Addon.ini'; + + if( !file_exists($ini_file) ){ + $error = sprintf($langmessage['File_Not_Found'],' '.$ini_file.''); + return false; + } + + + $this->ini_text = file_get_contents($ini_file); + $this->ini_contents = \gp\tool\Ini::ParseString($this->ini_text); + $this->ini_contents['source_folder'] = dirname($ini_file); + + + if( !$this->ini_contents ){ + $error = $langmessage['Ini_Error'].' '.$langmessage['Ini_Submit_Bug']; + $error = preg_replace('#href="[^"]+"#','href="' . \CMS_DOMAIN . '/Docs/Addon.ini"',$error); + return false; + } + + $this->HasHooks(); + + if( !isset($this->ini_contents['Addon_Name']) ){ + $error = $langmessage['Ini_No_Name'].' '.$langmessage['Ini_Submit_Bug']; + return false; + } + + if( isset($this->ini_contents['Addon_Unique_ID']) && !is_numeric($this->ini_contents['Addon_Unique_ID']) ){ + $error = 'Invalid Unique ID'; + return false; + } + + // Check Versions + if( !empty($this->ini_contents['min_gpeasy_version']) && version_compare($this->ini_contents['min_gpeasy_version'], \gpversion,'>') ){ + $error = sprintf($langmessage['min_version'],$this->ini_contents['min_gpeasy_version']).' '.$langmessage['min_version_upgrade']; + return false; + } + + + $this->display_name = $this->ini_contents['Addon_Name']; + + return true; + } + + + /** + * Does it have addon hooks? + * + */ + public function HasHooks(){ + foreach($this->ini_contents as $key => $value){ + if( is_array($value) && $key != 'FrontEndFramework' ){ + $this->has_hooks = true; + return; + } + } + + } + + + /** + * Parse the ini a second time with variables + * + */ + public function IniContents(){ + global $dataDir, $dirPrefix; + $folder = basename($this->dest); + + $variables = array( + '{$addon}' => $folder, + '{$plugin}' => $folder, + '{$dataDir}' => $dataDir, + '{$dirPrefix}' => $dirPrefix, + '{$addonRelativeData}' => \gp\tool::GetDir('/data/_addondata/'.$this->data_folder), + '{$addonRelativeCode}' => \gp\tool::GetDir($this->addon_folder_rel.'/'.$folder), + ); + + $this->ini_contents = \gp\tool\Ini::ParseString($this->ini_text,$variables); + } + + + /** + * Add hooks to configuration + * + */ + public function Hooks(){ + global $langmessage, $config; + + if( !$this->has_hooks ){ + return true; + } + + //needs to be before other gadget functions + $installedGadgets = $this->GetInstalledComponents($config['gadgets'],$this->config_key); + + $gadgets = $this->ExtractFromInstall($this->ini_contents,'Gadget:'); + $gadgets = $this->CleanGadgets($gadgets); + $this->PurgeExisting($config['gadgets'],$gadgets); + $this->AddToConfig($config['gadgets'],$gadgets); + + //remove gadgets that were installed but are no longer part of package + $gadgetNames = array_keys($gadgets); + $toRemove = array_diff($installedGadgets,$gadgetNames); + $this->RemoveFromHandlers($toRemove); + + //add new gadgets to GetAllGadgets handler + $toAdd = array_diff($gadgetNames,$installedGadgets); + $this->AddToHandlers($toAdd); + + + //admin links + $Admin_Links = $this->ExtractFromInstall($this->ini_contents,'Admin_Link:'); + $Admin_Links = $this->CleanLinks($Admin_Links,'Admin_'); + $this->PurgeExisting($config['admin_links']); + $this->AddToConfig($config['admin_links'],$Admin_Links); + + + + //special links + $Special_Links = $this->ExtractFromInstall($this->ini_contents,'Special_Link:'); + $Special_Links = $this->CleanLinks($Special_Links,'Special_','special'); + $this->AddToConfig_Special($Special_Links); + + + //generic hooks + $this->AddHooks(); + + return true; + } + + + /** + * Create a layout + * + */ + public function Layout(){ + global $gpLayouts, $langmessage, $config, $page; + + if( empty($this->new_layout) ){ + return true; + } + + if( $this->has_hooks ){ + $this->new_layout['addon_key'] = $this->config_key; + } + if( isset($this->ini_contents['Addon_Unique_ID']) && is_numeric($this->ini_contents['Addon_Unique_ID']) ){ + $this->new_layout['addon_id'] = $this->ini_contents['Addon_Unique_ID']; + } + if( isset($this->ini_contents['Addon_Version']) ){ + $this->new_layout['version'] = $this->ini_contents['Addon_Version']; + } + if( isset($this->ini_contents['Addon_Name']) ){ + $this->new_layout['name'] = $this->ini_contents['Addon_Name']; + } + if( isset($this->ini_contents['FrontEndFramework']) && is_array($this->ini_contents['FrontEndFramework']) ){ + $this->new_layout['framework'] = $this->ini_contents['FrontEndFramework']; + } + + $temp = $this->TempFile(); + $layout_id = basename($temp); + $gpLayouts[$layout_id] = $this->new_layout; + + if( $this->default_layout ){ + $config['gpLayout'] = $layout_id; + } + + return true; + } + + + /** + * Rename the temp folder to the dest folder + * + */ + public function FinalizeFolder(){ + + if( !$this->remote_install ){ + return true; + } + + if( file_exists($this->dest) ){ + $this->trash_path = $this->TempFile(); + if( !@rename($this->dest,$this->trash_path) ){ + $this->message('Existing destination not renamed'); + return false; + } + } + + //rename temp folder + if( rename($this->source,$this->dest) ){ + return true; + } + + $this->message('Couldn\'t rename to destination'); + return false; + } + + + /** + * Finalize the configuration + * + * + */ + public function FinalizeConfig(){ + global $langmessage, $config; + + if( !$this->has_hooks ){ + return true; + } + + //code folder + $this->config[$this->config_key]['code_folder_part'] = $this->addon_folder_rel.'/'.$this->dest_name; + $this->config[$this->config_key]['data_folder'] = $this->data_folder; + + + //general configuration + $this->UpdateConfigInfo('Addon_Name','name'); + $this->UpdateConfigInfo('Addon_Version','version'); + $this->UpdateConfigInfo('Addon_Unique_ID','id'); + $this->UpdateConfigInfo('Namespace','Namespace'); + + + //remote + unset($this->config[$this->config_key]['remote_install']); + if( $this->remote_install ){ + $this->config[$this->config_key]['remote_install'] = true; + } + + //layout + if( count($this->new_layout) ){ + $this->config[$this->config_key]['is_theme'] = true; + } + + + //proof of purchase + if( isset($this->ini_contents['Proof of Purchase']) && isset($this->ini_contents['Proof of Purchase']['order']) ){ + $this->order = $this->ini_contents['Proof of Purchase']['order']; + $this->config[$this->config_key]['order'] = $this->order; + } + + + $this->UpdateConfigInfo('editable_text','editable_text'); + $this->UpdateConfigInfo('About','About'); + $this->UpdateConfigInfo('html_head','html_head'); + + return true; + } + + + /** + * + * + */ + public function UpdateHistory(){ + + if( !$this->has_hooks ){ + return; + } + + // History + $history = array(); + $history['name'] = $this->config[$this->config_key]['name']; + $history['action'] = 'installed'; + if( isset($this->config[$this->config_key]['id']) ){ + $history['id'] = $this->config[$this->config_key]['id']; + } + $history['time'] = time(); + + $this->addonHistory[] = $history; + $this->SaveAddonData(); + + } + + + /** + * Run the Install_Check.php file if it exists + * @return bool + * + */ + public function CheckFile(){ + + // debug('Installer obj = ' . pre(get_object_vars($this))); // TODO remove + + $check_file = $this->source . '/Install_Check.php'; + if( !file_exists($check_file) ){ + return true; + } + $success = true; + + ob_start(); + include($check_file); + if( function_exists('Install_Check') ){ + $success = Install_Check(); + } + $msg = ob_get_clean(); + if( !empty($msg) ){ + $this->message($msg); + } + + return $success; + } + + + /** + * Return the path of a non-existant file + * Make sure the name won't conflict with names of addons or layouts + * + */ + public function TempFile(){ + global $config, $gpLayouts, $dataDir; + + do{ + $file = \gp\tool::RandomString(7,false); + $full_dest = $this->addon_folder.'/'.$file; + $data_dest = $dataDir.'/data/_addondata/'.$file; + + }while( + is_numeric($file) + || array_key_exists($file, $config['addons']) + || array_key_exists($file, $config['themes']) + || array_key_exists($file, $gpLayouts) + || file_exists($full_dest) + || file_exists($data_dest) + ); + + return $full_dest; + } + + + + /** + * Recursive copy folder + * + */ + public static function CopyAddonDir($fromDir,$toDir){ + + if( !\gp\tool\Files::CheckDir($toDir) ){ + return 'Copy failed: '.$fromDir.' to '.$toDir; + } + + $files = scandir($fromDir); + if( $files === false ){ + return 'scandir failed: '.$fromDir; + } + + + foreach($files as $file){ + + if( strpos($file,'.') === 0){ + continue; + } + + $fullFrom = $fromDir.'/'.$file; + $fullTo = $toDir.'/'.$file; + + + //directories + if( is_dir($fullFrom) ){ + $result = self::CopyAddonDir($fullFrom,$fullTo); + if( $result !== true ){ + return $result; + } + continue; + } + + //files + //If the destination file already exists, it will be overwritten. + if( !copy($fullFrom,$fullTo) ){ + return 'Copy failed: '.$fullFrom.' to '.$fullTo.' (2)'; + } + } + + return true; + } + + + /** + * Undo changes + * + */ + public function Failed(){ + global $config, $gpLayouts; + + if( is_array($this->config_cache) ){ + $config = $this->config_cache; + $gpLayouts = $this->layouts_cache; + } + + if( isset($this->trash_path) && file_exists($this->trash_path) ){ + @rename($this->trash_path,$this->dest); + } + + } + + public function message($message){ + $this->messages[] = $message; + } + + /** + * Get a stored order/purchase id + * @param int $id addon id + * + */ + public function GetOrder($id){ + if( !is_numeric($id) ){ + return; + } + + foreach( $this->config as $folder => $info ){ + if( !empty($info['id']) + && $id == $info['id'] + && !empty($info['order']) + ){ + return $info['order']; + } + } + } + + + /** + * Get the remote package + * + */ + public function GetRemote(){ + global $langmessage, $dataDir; + + + // download url + $download_url = \gp\admin\Tools::RemoteUrl( $this->type ); + + // allowed to remote install? + if( $download_url === false ){ + $this->message($langmessage['OOPS'].' (Can\'t remote install '.$this->type.')'); + return false; + } + + $download_url .= '?cmd=install&id='.rawurlencode($this->id); + + // purchase order id + if( is_null($this->order) ){ + $this->order = $this->GetOrder($this->id); + } + + if( !is_null($this->order) ){ + $download_url .= '&order='.rawurlencode($this->order); + } + + + // able to remote install? + if( !\gp\admin\Tools::CanRemoteInstall() ){ + $this->message($langmessage['OOPS'].' (Can\'t remote install)'); + return false; + } + + + // get package from remote + $getter = new \gp\tool\RemoteGet(); + $full_result = $getter->Get($download_url); + if( (int)$full_result['response']['code'] < 200 && (int)$full_result['response']['code'] >= 300 ){ + $this->message( $langmessage['download_failed'] .' (1)'); + return false; + } + + // download failed and a message was sent + if( isset($full_result['headers']['x-error']) ){ + $this->message( htmlspecialchars($full_result['headers']['x-error']) ); + $this->message( sprintf($langmessage['download_failed_xerror'],'href="'.self::DetailUrl($_POST['type'],$_POST['id']).'" data-cmd="remote"') ); + return false; + } + + $result = $full_result['body']; + $md5 =& $full_result['headers']['x-md5']; + $package_md5 = md5($result); + + //check md5 + if( $package_md5 != $md5 ){ + $this->message( $langmessage['download_failed_md5'].'
    (Package Checksum '.$package_md5.' != Expected Checksum '.$md5.')' ); + return false; + } + + //save contents + $tempfile = $dataDir.\gp\tool\FileSystem::TempFile('/data/_temp/addon','.zip'); + if( !\gp\tool\Files::Save($tempfile,$result) ){ + $this->message( $langmessage['download_failed'].' (Package not saved)' ); + return false; + } + + $this->source = $this->TempFile(); + $success = $this->ExtractArchive($tempfile); + + unlink($tempfile); + + return $success; + } + + + + /** + * Write Archive + * + */ + private function ExtractArchive($archive_path){ + global $langmessage, $dataDir; + + $archive = new \gp\tool\Archive($archive_path); + $extract_temp = $dataDir.\gp\tool\FileSystem::TempFile('/data/_temp/addon'); + if( !$archive->extractTo($extract_temp) ){ + $this->message( $langmessage['download_failed'].' (Package not extracted)' ); + return false; + } + + //get archive root + $archive_root = $archive->GetRoot(); + if( is_null($archive_root) ){ + $this->message( $langmessage['download_failed'].' (Root not found)' ); + return false; + } + + //rename to source folder + $rename_from = $extract_temp.'/'.ltrim($archive_root,'/'); + if( !\gp\tool\Files::Replace($rename_from, $this->source) ){ + $this->message( $langmessage['download_failed'].' (Not replaced)' ); + return false; + } + + return true; + } + + + /** + * Set config value based on ini setting + * + */ + public function UpdateConfigInfo($ini_var,$config_var){ + + if( isset($this->ini_contents[$ini_var]) ){ + $this->config[$this->config_key][$config_var] = $this->ini_contents[$ini_var]; + }elseif( isset($this->config[$this->config_key][$config_var]) ){ + unset($this->config[$this->config_key][$config_var]); + } + } + + + + /** + * Add an addon's special links to the configuration + * + */ + public function AddToConfig_Special($Special_Links){ + global $gp_index, $gp_titles, $gp_menu, $langmessage; + + $lower_links = array_change_key_case($Special_Links,CASE_LOWER); + + //purge links no longer defined ... similar to PurgeExisting() + foreach($gp_index as $linkName => $index){ + + $linkInfo = $gp_titles[$index]; + if( !isset($linkInfo['addon']) ){ + continue; + } + + if( $linkInfo['addon'] !== $this->config_key ){ + continue; + } + + if( isset($lower_links[$index]) ){ + continue; + } + + unset($gp_index[$linkName]); + unset($gp_titles[$index]); + if( isset($gp_menu[$index]) ){ + unset($gp_menu[$index]); + } + } + + + //prepare a list with all titles converted to lower case + $lower_titles = array_change_key_case($gp_index, CASE_LOWER); + + + //add new links ... similar to AddToConfig() + foreach($Special_Links as $new_title => $linkInfo){ + + $index = strtolower($new_title); + $title = \gp\tool::IndexToTitle($index); + + //if the title already exists, see if we need to update it + if( $title ){ + + $add_link = $this->CanAddLink( $gp_titles[$index] ); + + if( !$add_link ){ + $this->message( sprintf($langmessage['addon_key_defined'],' Special_Link: '.$new_title.'') ); + continue; + } + + //this will overwrite things like label which are at times editable by users + //$AddTo[$new_title] = $linkInfo + $AddTo[$new_title]; + + // if it doesn't exist, just add it + }else{ + + // we don't need the Special_ prefix, but we don't want duplicates + $temp = $new_title = substr($new_title,8); + $temp_lower = $new_lower = strtolower($new_title); + $i = 1; + while( isset($lower_titles[$new_lower]) ){ + $new_lower = $temp_lower.'_'.$i; + $new_title = $temp.'_'.$i; + $i++; + } + + $gp_index[$new_title] = $index; + $gp_titles[$index] = $linkInfo; + } + + + $this->UpdateLinkInfo($gp_titles[$index],$linkInfo); + } + } + + + + public function AddHooks(){ + + $installed = array(); + foreach($this->ini_contents as $hook => $hook_args){ + if( !is_array($hook_args) ){ + continue; + } + + if( strpos($hook, 'Gadget:') === 0 + || strpos($hook, 'Admin_Link:') === 0 + || strpos($hook, 'Special_Link:') === 0 + || strpos($hook, 'FrontEndFramework') === 0 + ){ + continue; + } + + if( $this->AddHook($hook, $hook_args) ){ + $installed[$hook] = $hook; + } + } + + $this->CleanHooks($this->config_key, $installed); + } + + + + public function AddHook($hook, $hook_args){ + global $config; + + $add = array(); + $this->UpdateLinkInfo($add, $hook_args); + $config['hooks'][$hook][$this->config_key] = $add; + + return true; + } + + + + //extract the configuration type (extractArg) from $Install + public function ExtractFromInstall(&$Install,$extractArg){ + if( !is_array($Install) || (count($Install) <= 0) ){ + return array(); + } + + $extracted = array(); + $removeLength = strlen($extractArg); + + foreach($Install as $InstallArg => $ArgInfo){ + if( strpos($InstallArg,$extractArg) !== 0 ){ + continue; + } + $extractName = substr($InstallArg,$removeLength); + if( !$this->CheckName($extractName) ){ + continue; + } + + $extracted[$extractName] = $ArgInfo; + } + return $extracted; + } + + + /* + * Add to $AddTo + * Don't add elements already defined by the cms or other addons + * + */ + public function AddToConfig(&$AddTo,$New_Config){ + global $langmessage; + + if( !is_array($New_Config) || (count($New_Config) <= 0) ){ + return; + } + + $lower_add_to = array_change_key_case($AddTo,CASE_LOWER); + + foreach($New_Config as $Config_Key => $linkInfo){ + + $lower_key = strtolower($Config_Key); + + if( isset($lower_add_to[$lower_key]) ){ + + $add_link = $this->CanAddLink( $lower_add_to[$lower_key] ); + + if( !$add_link ){ + $this->message( sprintf($langmessage['addon_key_defined'],' '.$Config_Key.'') ); + continue; + } + + //this will overwrite things like label which are at times editable by users + //$AddTo[$Config_Key] = $linkInfo + $AddTo[$Config_Key]; + + }else{ + $AddTo[$Config_Key] = $linkInfo; + } + + $this->UpdateLinkInfo($AddTo[$Config_Key],$linkInfo); + } + } + + public function CanAddLink( $info ){ + + if( !isset($info['addon']) || $this->config_key === false ){ + return false; + } + + if( $info['addon'] != $this->config_key ){ + return false; + } + return true; + } + + + public function UpdateLinkInfo(&$link_array,$new_info){ + + unset($link_array['script'], $link_array['data'], $link_array['class'], $link_array['method'], $link_array['value'], $link_array['class_admin']); + + $new_info += array('script'=>'','data'=>'','class'=>'','method'=>'','value'=>'','class_admin'=>''); + + + $link_array['addon'] = $this->config_key; + $link_array['class'] = $new_info['class']; + $link_array['value'] = $new_info['value']; + $link_array['class_admin'] = $new_info['class_admin']; + $link_array = array_filter($link_array); //remove empty values + + + if( !empty($new_info['script']) ){ + $link_array['script'] = $this->addon_folder_rel.'/'.$this->dest_name .'/'.$new_info['script']; + } + + if( !empty($new_info['data']) ){ + $link_array['data'] = '/data/_addondata/'.$this->data_folder.'/'.$new_info['data']; + } + + + if( !empty($new_info['method']) ){ + + $link_array['method'] = $new_info['method']; + if( strpos($link_array['method'],'::') > 0 ){ + $link_array['method'] = explode('::',$link_array['method']); + } + } + + } + + + /** + * Purge Links from $purgeFrom that were once defined for $this->config_key + * + */ + public function PurgeExisting(&$purgeFrom,$NewLinks = array()){ + + if( $this->config_key === false || !is_array($purgeFrom) ){ + return; + } + + foreach($purgeFrom as $linkName => $linkInfo){ + if( !isset($linkInfo['addon']) ){ + continue; + } + if( $linkInfo['addon'] !== $this->config_key ){ + continue; + } + + if( isset($NewLinks[$linkName]) ){ + continue; + } + + unset($purgeFrom[$linkName]); + } + + } + + + /** + * Make sure the extracted links are valid + * + */ + public function CleanLinks($links,$prefix,$linkType=null){ + + if( !is_array($links) ){ + return array(); + } + + $result = array(); + foreach($links as $linkName => $linkInfo){ + + if( !$this->ValidInfo($linkInfo) ){ + continue; + } + + if( stripos($linkName,$prefix) !== 0 ){ + $linkName = $prefix.$linkName; + } + + $result[$linkName] = $linkInfo; + + if( is_string($linkType) ){ + $result[$linkName]['type'] = $linkType; + } + } + + return $result; + } + + + /** + * similar to CleanLinks() + * + */ + public function CleanGadgets($gadgets){ + global $gpOutConf, $langmessage, $config; + + if( !is_array($gadgets) ){ + return array(); + } + + $result = array(); + foreach($gadgets as $gadgetName => $gadgetInfo){ + + //check against $gpOutConf + if( isset($gpOutConf[$gadgetName]) ){ + $this->message( sprintf($langmessage['addon_key_defined'],' Gadget: '.$gadgetName.'') ); + continue; + } + + //check against other gadgets + if( isset($config['gadgets'][$gadgetName]) && ($config['gadgets'][$gadgetName]['addon'] !== $this->config_key) ){ + $this->message( sprintf($langmessage['addon_key_defined'],' Gadget: '.$gadgetName.'') ); + continue; + } + + if( !$this->ValidInfo($gadgetInfo, false) ){ + continue; + } + + $result[$gadgetName] = $gadgetInfo; + } + + return $result; + } + + + /** + * Check the info to make sure it has a label and some way of executing code + * + */ + public function ValidInfo($info, $require_label = true ){ + + if( $require_label && empty($info['label']) ){ + return false; + } + + $keys = array('class','class_admin','data','method','script'); + foreach($keys as $key){ + if( !empty($info[$key]) ){ + return true; + } + } + + return false; + } + + + + /** + * Add gadgets to gpLayouts + * + */ + public function AddToHandlers($gadgets){ + global $gpLayouts; + + if( !is_array($gpLayouts) || !is_array($gadgets) ){ + return; + } + + foreach($gpLayouts as $layout => $containers){ + if( !is_array($containers) ){ + continue; + } + + if( isset($containers['handlers']['GetAllGadgets']) ){ + $container =& $gpLayouts[$layout]['handlers']['GetAllGadgets']; + if( !is_array($container) ){ + $container = array(); + } + $container = array_merge($container,$gadgets); + } + } + } + + + public function CheckName($name){ + + $test = str_replace(array('.','_',' '),array(''),$name ); + if( empty($test) || !ctype_alnum($test) ){ + $this->message( 'Could not install '.$name.'. Link and gadget names can only contain alphanumeric characters with underscore "_", dot "." and space " " characters.'); + return false; + } + return true; + } + + + /** + * Remove unused code folders created by incomplete addon installations + * + */ + public function CleanInstallFolder(){ + + if( !$this->remote_install ){ + return; + } + + if( file_exists($this->source) ){ + \gp\tool\Files::RmAll($this->source); + } + + if( file_exists($this->trash_path) ){ + \gp\tool\Files::RmAll($this->trash_path); + } + } + + + public function RemoveFromConfig(&$configFrom,$addon){ + + if( !is_array($configFrom) ){ + return; + } + foreach($configFrom as $key => $value ){ + if( !isset($value['addon']) ){ + continue; + } + if( $value['addon'] == $addon ){ + unset($configFrom[$key]); + } + } + } + + +} diff --git a/include/admin/Addon/Remote.php b/include/admin/Addon/Remote.php new file mode 100644 index 0000000..7ec0665 --- /dev/null +++ b/include/admin/Addon/Remote.php @@ -0,0 +1,298 @@ +SearchOptionSave(); + + //make a list of installed addon id's + $this->installed_ids = self::InstalledIds(); + + + //search settings + $this->searchUrl = $this->path_remote; + $this->searchOrderOptions['rating_score'] = $langmessage['Highest Rated']; + $this->searchOrderOptions['downloads'] = $langmessage['Most Downloaded']; + $this->searchOrderOptions['modified'] = $langmessage['Recently Updated']; + $this->searchOrderOptions['created'] = $langmessage['Newest']; + + + $_GET += array('q'=>''); + $this->searchPage = \gp\special\Search::ReqPage('page'); + + + //version specific search + if( !isset($config['search_version']) || $config['search_version'] ){ + $this->searchQuery .= '&ug='.rawurlencode(\gpversion); + } + + if( !empty($_GET['q']) ){ + $this->searchQuery .= '&q='.rawurlencode($_GET['q']); + } + + $this->SearchOrder(); // \gp\Addon\Install + + $slug = 'Plugins'; + if( $this->config_index == 'themes' ){ + $slug = 'Themes'; + } + $src = \addon_browse_path.'/'.$slug.'?cmd=remote&format=json&'.$this->searchQuery.'&page='.$this->searchPage; // format=json added 4.6b3 + + $this->ShowHeader(); // \gp\Addon\Install + + $data = $this->RemoteBrowseResponse($src); + if( $data === false ){ + return; + } + + $this->searchMax = $data['max']; + if( isset($data['per_page']) && $data['per_page'] ){ + $this->searchPerPage = $data['per_page']; + }else{ + $this->searchPerPage = count($data['rows']); + } + + + $this->RemoteBrowseRows($data); + + $this->VersionOption(); + } + + + + /** + * Output the rows found by a RemoteBrowse search + * + */ + public function RemoteBrowseRows($data){ + global $langmessage; + + if( count($data['rows']) == 0 ){ + echo '
    '; + echo '

    '.$langmessage['Sorry, nothing matched'].'

    '; + echo '
    '; + return; + } + + $this->SearchOptions(); // \gp\Addon\Install + + echo ''; + echo ''; + + foreach($data['rows'] as $row){ + echo ''; + echo ''; + } + echo '
    '.$langmessage['name'].''.$langmessage['version'].''.$langmessage['Statistics'].''.$langmessage['description'].'
    '; + echo self::DetailLink($row['type'], $row['id'], '','',' class="shot"'); + echo ''; + echo ''.$row['name'].''; + echo '
    '; + echo self::DetailLink($row['type'], $row['id'] ); + echo ' | '; + $this->InstallLink($row); + echo '
    '; + echo $row['version']; + echo ''; + echo sprintf($langmessage['_downloads'],number_format($row['downloads'])); + echo '
    '; + $this->CurrentRating($row['rating_weighted']); + echo '
    '; + echo $row['rating_count'].' ratings'; + echo '
    '; + echo $row['short_description']; + echo '
    '; + $this->SearchNavLinks(); + } + + + public function CurrentRating($rating){ + + $width = 16*5; + $pos = min($width,ceil($width*$rating)); + $pos2 = ($width-ceil($pos)); + + echo ''; + echo ''; + echo ''; + echo ' '; + } + + + /** + * Save the search option + * + */ + private function SearchOptionSave(){ + global $config; + + if( !isset($_GET['search_option']) ){ + return; + } + + switch($_GET['search_option']){ + case 'version': + unset($config['search_version']); + break; + case 'noversion': + $config['search_version'] = false; + break; + + default: + return; + } + + \gp\admin\Tools::SaveConfig(); + + } + + + + /** + * Return the list of installed addon ids + * + */ + public static function InstalledIds(){ + global $config; + + $ids = array(); + + if( isset($config['addons']) && is_array($config['addons']) ){ + foreach($config['addons'] as $addon_info){ + if( isset($addon_info['id']) ){ + $ids[] = $addon_info['id']; + } + } + } + return $ids; + } + + + + /** + * Get cached data or fetch new response from server and cache it + * + */ + public function RemoteBrowseResponse($src){ + global $dataDir, $langmessage; + + $cache_file = $dataDir.'/data/_remote/'.sha1($src).'.txt'; + $cache_used = false; + + //check cache + if( file_exists($cache_file) && (filemtime($cache_file)+ 26100) > time() ){ + $result = file_get_contents($cache_file); + $cache_used = true; + }else{ + $result = \gp\tool\RemoteGet::Get_Successful($src); + } + + $data = $this->ParseResponse($result); + + if( $data === false ){ + $this->ViewOnline(); + return false; + } + + //not unserialized? + if( count($data) == 0 ){ + echo '

    '; + echo $langmessage['search_no_results']; + echo '

    '; + return false; + } + + //save the cache + if( !$cache_used ){ + \gp\tool\Files::Save($cache_file,$result); + } + + return $data; + } + + + + /** + * Display option to limit search results to addons that are compat with the current cms version + * + */ + public function VersionOption(){ + global $langmessage, $config; + + echo '

    '.$langmessage['options'].'

    '; + echo '

    '; + echo 'Limit results to addons that are compatible with your version of '.\CMS_NAME.' ('.\gpversion.')   '; + + if( !isset($config['search_version']) || $config['search_version'] ){ + echo ''.$langmessage['On'].'   '; + echo \gp\tool::Link($this->searchUrl,$langmessage['Off'],$this->searchQuery.'&search_option=noversion',' data-cmd="gpajax"'); + + }else{ + echo \gp\tool::Link($this->searchUrl,$langmessage['On'],$this->searchQuery.'&search_option=version',' data-cmd="gpajax"'); + echo '   '.$langmessage['Off'].''; + } + echo '

    '; + + $this->ViewOnline(); + } + + + /** + * Convert the response string to an array + * Serialized or json (serialized data may be cached) + * + */ + protected function ParseResponse($result){ + + //no response + if( !$result ){ + echo '

    '.\gp\tool\RemoteGet::Debug('Sorry, data not fetched').'

    '; + return false; + } + + $data = false; + if( strpos($result,'a:') === 0 ){ + $data = unserialize($result); + + }elseif( strpos($result,'{') === 0 ){ + $data = json_decode($result,true); + } + + if( !is_array($data) ){ + $debug = array(); + $debug['Two'] = substr($result,0,2); + $debug['Twotr'] = substr(trim($result),0,2); + echo '

    '.\gp\tool\RemoteGet::Debug('Sorry, data not fetched',$debug).'

    '; + return false; + } + + return $data; + } + + + /** + * Link to view search resuls on typesettercms.com + * + */ + public function ViewOnline(){ + $slug = 'Plugins'; + if( $this->config_index == 'themes' ){ + $slug = 'Themes'; + } + $url = \addon_browse_path.'/'.$slug.'?'.$this->searchQuery.'&page='.$this->searchPage; + echo '

    View search results on '.CMS_READABLE_DOMAIN.'

    '; + } +} diff --git a/include/admin/Addon/Tools.php b/include/admin/Addon/Tools.php new file mode 100644 index 0000000..8db9ea6 --- /dev/null +++ b/include/admin/Addon/Tools.php @@ -0,0 +1,775 @@ +page ){ + $this->page->head_js[] = '/include/js/rate.js'; + } + + //clear the data file ... + $this->GetAddonData(); + } + + + + /** + * Get addon history and review data + * + */ + public function GetAddonData(){ + global $dataDir; + + $this->dataFile = $dataDir.'/data/_site/addonData.php'; + $addonData = \gp\tool\Files::Get('_site/addonData'); + + if( $addonData ){ + $this->addonHistory = $addonData['history']; + $this->addonReviews = $addonData['reviews']; + } + + } + + + + public function SaveAddonData(){ + + if( !isset($this->dataFile) ){ + trigger_error('dataFile not set'); + return; + } + + $addonData = array(); + + while( count($this->addonHistory) > 30 ){ + array_shift($this->addonHistory); + } + + $addonData['history'] = $this->addonHistory; + $addonData['reviews'] = $this->addonReviews; + return \gp\tool\Files::SaveData($this->dataFile, 'addonData', $addonData); + } + + + + /** + * Display clickable rating stars + * $arg is the addon id for plugins, folder for themes + * + */ + public function ShowRating($arg,$rating){ + + ob_start(); + echo ''; + + for($i = 1; $i < 6; $i++){ + + $attrs = array( + 'data-rating' => $i, + 'data-cmd' => 'gpabox', + ); + + if( $i > $rating ){ + $attrs['class'] = 'unset'; + } + + echo \gp\tool::Link( + $this->scriptUrl, + '', + 'cmd=ReviewAddonForm&rating=' . $i . '&arg=' . rawurlencode($arg), + $attrs + ); + } + + echo ''; + echo ' '; + + return ob_get_clean(); + } + + + + /** + * Return ini info if the addon is installable + * + * @return false|array + */ + public function GetAvailInstall($dir){ + global $langmessage; + + $iniFile = $dir . '/Addon.ini'; + $dirname = basename($dir); + + if( !file_exists($iniFile) ){ + + if( is_readable($dir) ){ + $this->invalid_folders[$dirname] = 'Addon.ini is not readable or does not exist'; + }else{ + $this->invalid_folders[$dirname] = 'Directory is not readable'; + } + + return false; + } + + $array = \gp\tool\Ini::ParseFile($iniFile); + if( $array === false ){ + $this->invalid_folders[$dirname] = $langmessage['Ini_Error']; + return false; + } + + if( !isset($array['Addon_Name']) ){ + $this->invalid_folders[$dirname] = $langmessage['Ini_No_Name']; + return false; + } + + $array += array('Addon_Version' => ''); + return $array; + } + + + + /** + * Manage addon ratings + * + */ + public function AdminAddonRating(){ + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'SendAddonReview'; + if( $this->SendAddonReview() ){ + return; + } + } + + $this->ReviewAddonForm(); + } + + + + public function ReviewAddonForm(){ + global $config, $dirPrefix, $langmessage; + + if( !$this->CanRate() ){ + return; + } + + $this->page->head_js[] = '/include/js/rate.js'; + + //get appropriate variables + $id = $this->addon_info['id']; + + if( isset($_REQUEST['rating']) ){ + $rating = $_REQUEST['rating']; + }elseif( isset($this->addonReviews[$id]) ){ + $rating = $this->addonReviews[$id]['rating']; + }else{ + $rating = 5; + } + + if( isset($_REQUEST['review']) ){ + $review = $_REQUEST['review']; + }elseif( isset($this->addonReviews[$id]) ){ + $review = $this->addonReviews[$id]['review']; + }else{ + $review = ''; + } + + echo '

    '; + echo $this->addon_info['name'].' » '.'Rate'; + echo '

    '; + + if( isset($this->addonReviews[$id]) ){ + echo 'You posted the following review on '; + echo \gp\tool::date($langmessage['strftime_date'], $this->addonReviews[$id]['time']); + } + + echo '
    '; + echo ''; + + echo ''; + echo ''; + + echo ''; + + $server = \gp\tool::ServerName(); + $host = $server.$dirPrefix; + + echo ''; + + echo ''; + + echo '
    Rating'; + + echo ''; + for($i = 1; $i < 6; $i++){ + $class = ''; + if( $i > $rating ){ + $class = ' class="unset"'; + } + echo ''; + } + echo ''; + echo ' '; + echo '
    Review'; + echo ''; + echo '
    From'; + echo ''; + echo '
    '; + echo ' '; + echo 'Click to hide your site information on ' . CMS_READABLE_DOMAIN . '.'; + echo '
    '; + + if( isset($this->addonReviews[$id]) ){ + echo ''; + }else{ + echo ''; + } + + echo ' '; + echo ''; + echo '
    '; + echo '
    '; + + return true; + } + + + + /** + * Get Addon info for rating + * Return true if it can be rated + * + */ + public function CanRate(){ + + $this->GetAddonData(); + + $arg =& $_REQUEST['arg']; + + switch($this->config_index){ + case 'themes': + $this->GetAddonRateInfoTheme($arg); + break; + case 'addons': + $this->GetAddonRateInfoPlugin($arg); + break; + default: + return false; + } + + if( !\gp\tool::IniGet('allow_url_fopen') ){ + $this->messages[] = 'Your installation of PHP does not support url fopen wrappers.'; + } + + if( count($this->messages) > 0 ){ + $message = 'Oops, you are currently unable to rate this addon for the following reasons:'; + $message .= '
      '; + $message .= '
    • ' . implode('
    • ', $this->messages) . '
    • '; + $message .= '
    '; + msg($message); + $this->ShowRatingText = false; + return false; + } + return true; + } + + + + public function GetAddonRateInfoTheme($dir){ + global $dataDir, $langmessage; + + $dir = str_replace('\\', '/', $dir); + $dir = str_replace('../', './', $dir); + $full_dir = $dataDir . $dir; + + if( !file_exists($full_dir) ){ + $this->messages[] = $langmessage['OOPS'] . ' (directory doesn\'t exist)'; + return false; + } + + $ini = $this->GetAvailInstall($full_dir); + + if( $ini === false ){ + $this->messages[] = 'This add-on does not have an ID assigned to it. The developer must update the install configuration.'; + return false; + } + + if( !isset($ini['Addon_Unique_ID']) ){ + $this->messages[] = 'This add-on does not have an ID assigned to it. The developer must update the install configuration.'; + return false; + } + + + $this->pass_arg = $dir; + $this->addon_info['id'] = $ini['Addon_Unique_ID']; + $this->addon_info['name'] = $ini['Addon_Name']; + + return true; + } + + + + public function GetAddonRateInfoPlugin($arg){ + global $config; + + if( isset($config['addons'][$arg]) && isset($config['addons'][$arg]['id']) ){ + $this->pass_arg = $config['addons'][$arg]['id'];; + $this->addon_info['id'] = $config['addons'][$arg]['id']; + $this->addon_info['name'] = $config['addons'][$arg]['name']; + return true; + } + + if( !is_numeric($arg) ){ + $this->messages[] = 'This add-on does not have an ID assigned to it. The developer must update the install configuration.'; + return false; + } + + foreach($config['addons'] as $addonDir => $data){ + if( isset($data['id']) && ($data['id'] == $arg) ){ + $this->pass_arg = $arg; + $this->addon_info['id'] = $data['id']; + $this->addon_info['name'] = $data['name']; + return true; + } + } + + foreach($this->addonHistory as $data ){ + if( isset($data['id']) && ($data['id'] == $arg) ){ + $this->pass_arg = $arg; + $this->addon_info['id'] = $data['id']; + $this->addon_info['name'] = $data['name']; + return true; + } + } + $this->messages[] = 'The supplied add-on ID is not in your add-on history.'; + return false; + } + + + + /** + * Send the addon rating to server + * + */ + public function SendAddonReview(){ + global $langmessage, $config, $dirPrefix; + $this->page->ajaxReplace = array(); + $data = array(); + + if( !$this->CanRate() ){ + return; + } + + if( !is_numeric($_POST['rating']) || ($_POST['rating'] < 1) || ($_POST['rating'] > 5 ) ){ + msg($langmessage['OOPS'] . ' (Invalid Rating)'); + return false; + } + + $id = $this->addon_info['id']; + + //don't send if it hasn't chagned + if( isset($this->addonReviews[$id]) ){ + $data['review_id'] = $this->addonReviews[$id]['review_id']; + + //if it hasn't changed.. + if( ($_POST['rating'] == $this->addonReviews[$id]['rating']) + && ($_POST['review'] == $this->addonReviews[$id]['review']) ){ + $this->ShowRatingText = false; + msg('Your review has been saved and will be posted pending approval.'); + return true; + } + } + + //send rating + $data['addon_id'] = $id; + $data['rating'] = (int)$_POST['rating']; + $data['review'] = $_POST['review']; + $data['cmd'] = 'rate'; + $data['HTTP_HOST'] = \gp\tool::ServerName(); + $data['SERVER_ADDR'] = $_SERVER['SERVER_ADDR']; + $data['dirPrefix'] = $dirPrefix; + if( isset($_POST['show_site']) && $_POST['show_site'] == 'hidden' ){ + $data['show_site'] = 'hidden'; + } + $review_id = $this->PingRating($data); + if( $review_id === false ){ + return false; + } + + //save review information + $this->addonReviews[$id] = array(); + $this->addonReviews[$id]['rating'] = (int)$_POST['rating']; + $this->addonReviews[$id]['review'] = substr($_POST['review'], 0, 500); + $this->addonReviews[$id]['review_id'] = $review_id; + $this->addonReviews[$id]['time'] = time(); + $this->SaveAddonData(); + + $this->ShowRatingText = false; + msg('Your review has been saved and will be posted pending approval.'); + return true; + } + + + + public function PingRating($data){ + + $path = CMS_DOMAIN . '/index.php/Special_Addons?' . http_build_query($data, '', '&'); + $contents = \gp\tool\RemoteGet::Get_Successful($path); + + return $this->RatingResponse($contents); + } + + + + public function RatingResponse($contents){ + global $langmessage; + if( empty($contents) ){ + msg($langmessage['OOPS'] . ' (empty rating)'); + return false; + } + + //!! these responses should be more detailed + list($response,$detail) = explode(':', $contents); + $response = trim($response); + $detail = trim($detail); + if( $response == 'successful_rating_request' ){ + return $detail; + } + + //invalid_rating_request + switch($detail){ + case 'no_addon'; + msg('The supplied addon id was invalid.'); + break; + + default: + msg($langmessage['OOPS'] . ' (Detail:' . htmlspecialchars($detail) . ')'); + //msg($contents); + break; + } + return false; + } + + + + /** + * Get a list of installed addons + * + */ + public function GetInstalledComponents($from, $addon){ + $result = array(); + if( !is_array($from) ){ + return $result; + } + + foreach($from as $name => $info){ + if( !isset($info['addon']) ){ + continue; + } + if( $info['addon'] !== $addon ){ + continue; + } + $result[] = $name; + } + return $result; + } + + + + //remove gadgets from $gpLayouts + public function RemoveFromHandlers($gadgets){ + global $gpLayouts; + + if( !is_array($gpLayouts) || !is_array($gadgets) ){ + return; + } + + foreach($gpLayouts as $theme => $containers){ + if( !is_array($containers) || !isset($containers['handlers']) || !is_array($containers['handlers']) ){ + continue; + } + foreach($containers['handlers'] as $container => $handlers){ + if( !is_array($handlers) ){ + continue; + } + + foreach($handlers as $index => $handle){ + $pos = strpos($handle, ':'); + if( $pos > 0 ){ + $handle = substr($handle, 0, $pos); + } + + foreach($gadgets as $gadget){ + if( $handle === $gadget ){ + $handlers[$index] = false; //set to false + } + } + } + + $handlers = array_diff($handlers, array(false)); //remove false entries + $handlers = array_values($handlers); //reset keys + $gpLayouts[$theme]['handlers'][$container] = $handlers; + } + } + } + + + + public function CleanHooks($addon, $keep_hooks=array()){ + global $config, $gp_hooks; + + if( !isset($config['hooks']) ){ + return; + } + + foreach($config['hooks'] as $hook_name => $hook_array){ + + foreach($hook_array as $hook_dir => $hook_args){ + + //not cleaning other addons + if( $hook_dir != $addon ){ + continue; + } + + if( !isset($keep_hooks[$hook_name]) ){ + unset($config['hooks'][$hook_name][$hook_dir]); + unset($gp_hooks[$hook_name][$hook_dir]); + //msg('remove this hook: ' . $hook_name); + } + } + } + + //reduce further if empty + foreach($config['hooks'] as $hook_name => $hook_array){ + if( empty($hook_array) ){ + unset($config['hooks'][$hook_name]); + unset($gp_hooks[$hook_name]); + } + } + + } + + + + /** + * Determine if the addon (identified by $ini_info and $source_folder) is an upgrade to an existing addon + * + * @return mixed + */ + public function UpgradePath($ini_info, $config_key='addons'){ + global $config, $dataDir; + + if( !isset($config[$config_key]) ){ + return false; + } + + //by id + if( isset($ini_info['Addon_Unique_ID']) ){ + foreach($config[$config_key] as $addon_key => $data){ + if( !isset($data['id']) || !is_numeric($data['id']) ){ + continue; + } + + if( (int)$data['id'] == (int)$ini_info['Addon_Unique_ID'] ){ + return $addon_key; + } + } + } + + //by name + if( isset($ini_info['Addon_Name']) ){ + foreach($config[$config_key] as $addon_key => $data){ + if( isset($data['name']) && $data['name'] == $ini_info['Addon_Name'] ){ + return $addon_key; + } + } + } + + //by path + if( isset($ini_info['source_folder']) ){ + foreach($config[$config_key] as $addon_key => $data){ + if( !isset($data['code_folder_part']) ){ + continue; + } + $source_folder = $dataDir.$data['code_folder_part']; + + if( $source_folder === $ini_info['source_folder'] ){ + return $addon_key; + } + } + } + + return false; + } + + + + public function AddonPanelGroup($addon_key, $show_hooks=true, $format=false){ + + $this->AddonPanel_Special($addon_key,$format); + $this->AddonPanel_Admin($addon_key,$format); + $this->AddonPanel_Gadget($addon_key,$format); + + if( $show_hooks ){ + $this->AddonPanel_Hooks($addon_key,$format); + } + } + + + + public function AdminLinkList($links, $label, $format){ + $_links = array(); + foreach($links as $linkName => $linkInfo){ + $_links[] = \gp\tool::Link($linkName, $linkInfo['label']); + } + $this->FormatList($_links, $label, $format); + } + + + + public function FormatList($links, $label, $format=false){ + if( empty($links) ){ + return; + } + + if( !$format ){ + $format = array(); + $format['start'] = '
  • %s (%s)'; + $format['end'] = '
  • '; + } + + echo sprintf($format['start'], $label, count($links)); + + echo '
      '; + foreach($links as $link){ + echo '
    • ' . $link . '
    • '; + } + echo '
    '; + echo $format['end']; + } + + + + //show Special Links + public function AddonPanel_Special($addon_key, $format){ + $sublinks = \gp\admin\Tools::GetAddonTitles($addon_key); + $this->AdminLinkList($sublinks, 'Special Links', $format); + } + + + + //show Admin Links + public function AddonPanel_Admin($addon_key, $format){ + global $langmessage, $config; + + $sublinks = \gp\admin\Tools::GetAddonComponents($config['admin_links'], $addon_key); + $this->AdminLinkList($sublinks, 'Admin Links', $format); + } + + + + //show Gadgets + public function AddonPanel_Gadget($addon_key, $format){ + global $langmessage, $config; + + $gadgets = \gp\admin\Tools::GetAddonComponents($config['gadgets'], $addon_key); + $links = array(); + foreach($gadgets as $name => $value){ + $links[] = $this->GadgetLink($name); + } + $this->FormatList($links, $langmessage['gadgets'], $format); + } + + + + //hooks + public function AddonPanel_Hooks($addon_key, $format){ + + $hooks = self::AddonHooks($addon_key); + $links = array(); + + foreach($hooks as $name => $hook_info){ + $links[] = '' + . str_replace('_', ' ', $name) . ''; + } + $this->FormatList($links,'Hooks',$format); + } + + + + /** + * Return array of hooks associated with the addon + * + */ + public static function AddonHooks($addon_key){ + global $config; + $hooks = array(); + + if( !isset($config['hooks']) || !is_array($config['hooks']) ){ + return $hooks; + } + foreach($config['hooks'] as $hook => $hook_array){ + if( isset($hook_array[$addon_key]) ){ + $hooks[$hook] = $hook_array[$addon_key]; + } + } + return $hooks; + } + + + + public static function DetailLink( $type, $id, $label='Details', $q='', $attr=''){ + return '' . $label . ''; + } + + + + public static function DetailUrl($type , $id , $q=''){ + $url = 'Themes'; + if( $type == 'plugins' ){ + $url = 'Plugins'; + } + if( !empty($q) ){ + $q = '?' . $q; + } + return addon_browse_path . '/' . $url . '/' . $id.$q; + } + +} diff --git a/include/admin/Addons.php b/include/admin/Addons.php new file mode 100644 index 0000000..2610235 --- /dev/null +++ b/include/admin/Addons.php @@ -0,0 +1,660 @@ +/Addon.ini + * - Addon_Name (required) + * - link definitions: (optional) + * - should be able to have multiple links, + * + * + * - link_name (required) + * an example link: Admin_ + * - labels (required) should pull the language values during installation/upgrading + * - script (required) + * - class / function to call once opened (optional) + * + * - minimum version / max version + * /addons// + */ + + +class Addons extends \gp\admin\Addon\Install{ + + public $dataFile; + + + public function __construct( $args ){ + global $langmessage; + + parent::__construct($args); + + $this->InitRating(); + $this->GetData(); + + $this->avail_addons = $this->GetAvailAddons(); + + } + + public function RunScript(){ + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + + case 'LocalInstall': + $this->LocalInstall(); + break; + + case 'remote_install': + case 'RemoteInstall': + $this->RemoteInstall(); + return; + case 'RemoteInstallConfirmed': + $this->RemoteInstallConfirmed(); + break; + + + case 'SendAddonReview': + case 'ReviewAddonForm': + $this->AdminAddonRating(); + if( $this->ShowRatingText ){ + return; + } + break; + + case 'enable': + case 'disable': + $this->GadgetVisibility($cmd); + return; + + case 'uninstall': + $this->Uninstall(); + return; + + case 'confirm_uninstall': + $this->Confirm_Uninstall(); + break; + } + + + + //single addon + $request_parts = explode('/',$this->page->requested); + if( count($request_parts) > 2 ){ + $this->ShowAddon($request_parts[2]); + return; + } + + + + $this->Select(); + $this->CleanAddonFolder(); + } + + + /** + * Remove unused code folders created by incomplete addon installations + * + */ + public function CleanAddonFolder(){ + global $config; + + + //get a list of all folders + $folder = '/data/_addoncode'; + $code_folders = $this->GetCleanFolders($folder); + $folder = '/data/_addondata'; + $data_folders = $this->GetCleanFolders($folder); + + //check against folders used by addons + $addons = $config['addons']; + foreach($addons as $addon_key => $info){ + $addon_config = \gp\tool\Plugins::GetAddonConfig($addon_key); + if( array_key_exists($addon_config['code_folder_part'],$code_folders) ){ + $code_folders[$addon_config['code_folder_part']] = false; + } + if( array_key_exists($addon_config['data_folder_part'],$data_folders) ){ + $data_folders[$addon_config['data_folder_part']] = false; + } + } + + //remove unused folders + $folders = array_filter($code_folders) + array_filter($data_folders); + foreach($folders as $folder => $full_path){ + \gp\tool\Files::RmAll($full_path); + } + + } + + + /** + * Get a list of folders within $dir that + * + */ + public function GetCleanFolders($relative){ + global $dataDir; + + $dir = $dataDir.$relative; + $folders = array(); + + if( file_exists($dir) ){ + $files = scandir($dir); + foreach($files as $file){ + if( $file == '.' || $file == '..' ){ + continue; + } + $full_path = $dir.'/'.$file; + if( !is_dir($full_path) ){ + continue; + } + $mtime = filemtime($full_path); + $diff = time() - $mtime; + if( $diff < 3600 ){ + continue; + } + $folders[$relative.'/'.$file] = $full_path; + } + } + return $folders; + } + + + + public function GadgetVisibility($cmd){ + global $config, $langmessage; + + $this->page->ajaxReplace = array(); + $gadget = $_GET['gadget']; + + if( !isset($config['gadgets']) || !is_array($config['gadgets']) || !isset($config['gadgets'][$gadget]) ){ + msg($langmessage['OOPS'].' (Invalid Gadget)'); + return; + } + + $gadgetInfo =& $config['gadgets'][$gadget]; + + switch($cmd){ + case 'enable': + unset($gadgetInfo['disabled']); + break; + case 'disable': + $gadgetInfo['disabled'] = true; + break; + + } + + if( !\gp\admin\Tools::SaveConfig(true) ){ + return; + } + + $link = $this->GadgetLink($gadget); + $this->page->ajaxReplace[] = array('replace','.gadget_link_'.md5($gadget),$link); + } + + public function GadgetLink($name){ + global $config, $langmessage; + $info =& $config['gadgets'][$name]; + if( !$info ){ + return ''; + } + + if( isset($info['disabled']) ){ + return \gp\tool::Link('Admin/Addons',str_replace('_',' ',$name).' ('.$langmessage['disabled'].')','cmd=enable&addon='.rawurlencode($info['addon']).'&gadget='.rawurlencode($name),'data-cmd="gpajax" class="gadget_link_'.md5($name).'"'); + }else{ + return \gp\tool::Link('Admin/Addons',str_replace('_',' ',$name) .' ('.$langmessage['enabled'].')','cmd=disable&addon='.rawurlencode($info['addon']).'&gadget='.rawurlencode($name),'data-cmd="gpajax" class="gadget_link_'.md5($name).'"'); + } + + } + + + /** + * Addon Data + * + */ + public function GetData(){ + global $dataDir,$config; + + //new + if( !isset($config['addons']) ){ + $config['addons'] = array(); + } + + if( !isset($config['admin_links']) ){ + $config['admin_links'] = array(); + } + + if( !isset($config['gadgets']) ){ + $config['gadgets'] = array(); + } + + + //fix data + $firstValue = current($config['addons']); + if( is_string($firstValue) ){ + + foreach($config['addons'] as $addon => $addonName){ + $config['addons'][$addon] = array(); + $config['addons'][$addon]['name'] = $addonName; + } + } + } + + + /** + * Prompt User about uninstalling an addon + */ + public function Uninstall(){ + global $config,$langmessage; + + echo '
    '; + echo '

    '.$langmessage['uninstall'].'

    '; + echo '
    '; + + $addon =& $_REQUEST['addon']; + if( !isset($config['addons'][$addon]) ){ + echo $langmessage['OOPS']; + echo '

    '; + echo ' '; + echo '

    '; + + }else{ + + echo $langmessage['confirm_uninstall']; + + echo '

    '; + echo ''; + echo ''; + echo ' '; + echo ' '; + echo '

    '; + } + + + echo '
    '; + echo '
    '; + } + + public function Confirm_Uninstall(){ + + $addon =& $_POST['addon']; + + $installer = new \gp\admin\Addon\Installer(); + $installer->Uninstall($addon); + $installer->OutputMessages(); + } + + + /** + * Display addon details + * + */ + public function ShowAddon($encoded_key){ + global $config, $langmessage; + + $addon_key = \gp\admin\Tools::decode64($encoded_key); + + if( !isset($config['addons'][$addon_key]) ){ + msg($langmessage['OOPS'].'(Addon Not Found)'); + $this->Select(); + return; + } + + $show = $this->GetDisplayInfo(); + $info = $show[$addon_key]; + + + $this->ShowHeader($info['name']); + + + $this->UpgradeLinks($info); + + //about + if( !empty($info['About']) ){ + echo '
    '; + echo '
    '; + echo $info['About']; + echo '
    '; + echo '

    '; + } + + + echo '
    '; + + $format = array(); + $format['end'] = '
    '; + $format['start'] = '

    %s

    '; + + $this->AddonPanel_Special($addon_key,$format); + $this->AddonPanel_Admin($addon_key,$format); + $this->AddonPanel_Gadget($addon_key,$format); + $this->AddonPanel_Hooks($addon_key, $format); + $this->OptionLinks($addon_key, $info, $format); + + + + echo '
    '; + + } + + + /** + * Get a list of available addons + * + */ + public function GetAvailAddons(){ + global $dataDir; + + $addonPath = $dataDir.'/addons'; + + if( !file_exists($addonPath) ){ + msg('Warning: The /addons folder "'.$addonPath.'" does not exist on your server.'); + return array(); + } + + + $folders = \gp\tool\Files::ReadDir($addonPath,1); + $versions = array(); + $avail = array(); + + foreach($folders as $value){ + $fullPath = $addonPath .'/'.$value; + $info = $this->GetAvailInstall($fullPath); + + if( !$info ){ + continue; + } + + + $info['source_folder'] = $addonPath .'/'. $value; + $info['upgrade_key'] = \gp\admin\Addon\Tools::UpgradePath($info); + $avail[$value] = $info; + + if( isset($info['Addon_Version']) && isset($info['Addon_Unique_ID']) ){ + + $id = $info['Addon_Unique_ID']; + if( !isset($versions[$id]) ){ + $versions[$id] = $info['Addon_Version']; + }elseif( version_compare($versions[$id],$info['Addon_Version'],'<') ){ + $versions[$id] = $info['Addon_Version']; + continue; + } + } + + } + + if( gp_unique_addons ){ + $avail = self::FilterUnique($avail, $versions); + } + + return $avail; + } + + + /** + * Filter the list of addons so we only have a list of the most recent versions + * + */ + protected static function FilterUnique($addons, $versions){ + + $temp = array(); + foreach($addons as $key => $info){ + + if( !isset($info['Addon_Version']) || !isset($info['Addon_Unique_ID']) ){ + $temp[$key] = $info; + continue; + } + + $id = $info['Addon_Unique_ID']; + $version = $info['Addon_Version']; + + if( version_compare($versions[$id], $version,'>') ){ + continue; + } + + $temp[$key] = $info; + } + + return $temp; + } + + + public function Instructions(){ + echo '
    '; + echo 'Plugin Documentation'; + } + + + /** + * Show installed and locally available plugins + * + */ + public function Select(){ + $this->ShowHeader(); + $this->ShowInstalled(); + $this->Instructions(); + } + + + /** + * Show installed addons + * + */ + public function ShowInstalled(){ + + $show = $this->GetDisplayInfo(); + + echo '
    '; + + // sort installed addons by name + $names = []; + foreach($show as $key => $row){ + $names[$key] = $row['name']; + } + // PHP 5.5+: $names = array_column($show, 'name'); + array_multisort($names, SORT_ASC, $show); + + foreach($show as $addon_key => $info){ + $this->PluginPanelGroup($addon_key,$info); + } + echo '
    '; + + return true; + } + + + /** + * Get addon configuration along with upgrade info + * + */ + public function GetDisplayInfo(){ + global $config; + + //show installed addons + $show = $config['addons']; + if( !is_array($show) ){ + return array(); + } + + + //set upgrade_from + foreach($this->avail_addons as $folder => $info){ + if( !$info['upgrade_key'] ){ + continue; + } + + $upgrade_key = $info['upgrade_key']; + if( !isset($show[$upgrade_key]) ){ + continue; + } + + + if( !isset($info['Addon_Version']) ){ + $show[$upgrade_key]['upgrade_from'] = $folder; + continue; + } + + if( !isset($show[$upgrade_key]['upgrade_version']) || version_compare($show[$upgrade_key]['upgrade_version'], $info['Addon_Version'], '<') ){ + $show[$upgrade_key]['upgrade_from'] = $folder; + $show[$upgrade_key]['upgrade_version'] = $info['Addon_Version']; + } + + } + + return $show; + } + + + public function PluginPanelGroup($addon_key,$info){ + global $langmessage, $gpLayouts; + + $addon_config = \gp\tool\Plugins::GetAddonConfig($addon_key); + + $addon_config += $info; //merge the upgrade info + + echo '
    '; + + echo '

    '; + echo \gp\tool::Link('Admin/Addons/'.\gp\admin\Tools::encode64($addon_key),$addon_config['name']); + echo '

    '; + + echo '
    '; + echo ''; + + $this->UpgradeLinks($addon_config); + + echo '
    '; + echo '
    '; + } + + + /** + * Plugin Upgrade links + * + */ + public function UpgradeLinks($addon_config){ + global $langmessage; + + //upgrade local + if( isset($addon_config['upgrade_from']) && isset($addon_config['upgrade_version']) ){ + if(version_compare($addon_config['upgrade_version'],$addon_config['version'] ,'>') ){ + echo '
    '; + $label = $langmessage['new_version'].'   '.$addon_config['upgrade_version']; + echo ''.$label.''; + echo '
    '; + } + } + + //upgrade cms + if( isset($addon_config['id']) && isset(\gp\admin\Tools::$new_versions[$addon_config['id']]) ){ + + $new_version = \gp\admin\Tools::$new_versions[$addon_config['id']]; + + if( version_compare($new_version['version'],$addon_config['version'],'>') ){ + + echo ''; + } + } + + } + + + /** + * Plugin option links + * + */ + public function OptionLinks($addon_key, $addon_config, $format = false){ + global $langmessage, $gpLayouts; + + $list = array(); + + if( !isset($addon_config['is_theme']) || !$addon_config['is_theme'] ){ + + //editable text + if( isset($addon_config['editable_text']) && \gp\admin\Tools::HasPermission('Admin_Theme_Content') ){ + $list[] = \gp\tool::Link('Admin_Theme_Content/Text',$langmessage['editable_text'],'cmd=AddonTextForm&addon='.urlencode($addon_key),array('title'=>urlencode($langmessage['editable_text']),'data-cmd'=>'gpabox')); + } + + //upgrade link + if( isset($addon_config['upgrade_from']) ){ + $list[] = ''.$langmessage['upgrade'].''; + } + + //uninstall + $list[] = \gp\tool::Link('Admin/Addons',$langmessage['uninstall'],'cmd=uninstall&addon='.rawurlencode($addon_key),'data-cmd="gpabox"'); + + + //version + if( !empty($addon_config['version']) ){ + $list[] = ''.$langmessage['Your_version'].' '.$addon_config['version']. ''; + } + + //rating + if( isset($addon_config['id']) && is_numeric($addon_config['id']) ){ + $id = $addon_config['id']; + + $rating = 5; + if( isset($this->addonReviews[$id]) ){ + $rating = $this->addonReviews[$id]['rating']; + } + $label = $langmessage['rate_this_addon'].' '.$this->ShowRating($id,$rating); + $list[] = ''.$label. ''; + } + + $this->FormatList($list,$langmessage['options'],$format); + return; + } + + //show list of themes using these addons + foreach($gpLayouts as $layout_id => $layout_info){ + if( !isset($layout_info['addon_key']) || $layout_info['addon_key'] !== $addon_key ){ + continue; + } + + $item = ' '; + $item .= \gp\tool::Link('Admin_Theme_Content',$layout_info['label']); + $item .= ' ( '; + $item .= \gp\tool::Link('Admin_Theme_Content/Edit/'.$layout_id,$langmessage['edit']); + $item .= ' )'; + + $list[] = $item; + } + + $this->FormatList($list,$langmessage['layouts'],$format); + } + + + /** + * Install Local Packages + * + */ + public function LocalInstall(){ + global $dataDir, $langmessage; + + $_REQUEST += array('source'=>''); + + if( !isset($this->avail_addons[$_REQUEST['source']]) ){ + msg($langmessage['OOPS'].' (Invalid Request)'); + return false; + } + + + $installer = new \gp\admin\Addon\Installer(); + $installer->source = $this->avail_addons[$_REQUEST['source']]['source_folder']; + + $installer->Install(); + $installer->OutputMessages(); + } + +} diff --git a/include/admin/Configuration.php b/include/admin/Configuration.php new file mode 100644 index 0000000..5f80bdf --- /dev/null +++ b/include/admin/Configuration.php @@ -0,0 +1,573 @@ +page->ajaxReplace = array(); + + + //add examples to smtp_hosts + $langmessage['about_config']['smtp_hosts'] .= 'ssl://smtp.gmail.com:465 ; tls://smtp.live.com:587'; + $langmessage['about_config']['showgplink'] = 'Showing the "powered by" link on your site is a great way to support '.CMS_NAME.' CMS.'; + $langmessage['about_config']['history_limit'] = 'Max: '.gp_backup_limit; + $langmessage['about_config']['maxthumbsize'] .= ' '.\gp\tool::Link('Admin/Configuration',$langmessage['recreate_all_thumbnails'],'cmd=recreate_thumbs','class="" data-cmd="creq"'); + + $this->variables = array( + + // these values aren't used + //'timeoffset'=>'', + //'dateformat'=>'', + + /* General Settings */ + 'general_settings' => false, + 'title' => '', + 'keywords' => '', + 'desc' => 'textarea', + + 'Interface' => false, + 'colorbox_style' => [ + 'minimalistic' => 'Minimalistic', + 'example1' => 'Example 1', + 'example2' => 'Example 2', + 'example3' => 'Example 3', + 'example4' => 'Example 4', + 'example5' => 'Example 5', + ], + 'gallery_legacy_style' => 'boolean', + 'language' => '', + 'langeditor' => '', + 'showsitemap' => 'boolean', + 'showlogin' => 'boolean', + 'showgplink' => 'boolean', + + 'Images' => false, + 'allow_svg_upload' => 'boolean', + 'maximgarea' => 'integer', + 'resize_images' => 'boolean', + 'preserve_icc_profiles' => 'boolean', + 'preserve_image_metadata' => 'boolean', + 'maxthumbsize' => 'integer', + 'maxthumbheight' => 'integer', + 'thumbskeepaspect' => 'boolean', + + 'Performance' => false, + 'auto_redir' => 'integer', + 'history_limit' => 'integer', + 'HTML_Tidy' => '', + 'Report_Errors' => 'boolean', + 'combinejs' => 'boolean', + 'minifyjs' => 'boolean', + 'combinecss' => 'boolean', + 'etag_headers' => 'boolean', + 'space_char' => [ + '_' => 'Underscore "_"', + '-' => 'Dash "-"' + ], + + /* Hide Admin UI Settings */ + 'Hide Admin UI' => false, + 'admin_ui_autohide_below' => 'integer', + 'admin_ui_hotkey' => '', + 'admin_ui_hotkey_code' => 'hidden', + + /* Contact Configuration */ + 'contact_config' => false, + 'toemail' => '', + 'toname' => '', + 'from_address' => '', + 'from_name' => '', + 'from_use_user' => 'boolean', + 'require_email' => '', + 'contact_advanced' => false, + 'mail_method' => '', + 'sendmail_path' => '', + 'smtp_hosts' => '', + 'smtp_user' => '', + 'smtp_pass' => 'password', + //'fromemail' => '', + + 'reCaptcha' => false, + 'recaptcha_public' => '', + 'recaptcha_private' => '', + 'recaptcha_language' => '', + ); + + } + + public function RunScript(){ + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'save_config': + $this->SaveConfig(); + break; + case 'recreate_thumbs': + $this->RecreateThumbs(); + break; + } + + $this->showForm(); + } + + + /** + * Save the posted configuration + * + */ + protected function SaveConfig(){ + global $config, $langmessage; + + $config_before = $config; + $possible = $this->getPossible(); + + foreach($possible as $key => $curr_possible){ + + if( $curr_possible == 'boolean' ){ + if( isset($_POST[$key]) && ($_POST[$key] == 'true') ){ + $config[$key] = true; + }else{ + $config[$key] = false; + } + + }elseif( $curr_possible == 'integer' ){ + if( isset($_POST[$key]) && ( is_numeric($_POST[$key]) || $_POST[$key] == '' ) ){ // also allow empty values + $config[$key] = $_POST[$key]; + } + + }elseif( isset($_POST[$key]) ){ + $config[$key] = $_POST[$key]; + } + } + + + $config['history_limit'] = min($config['history_limit'],gp_backup_limit); + + if( !\gp\admin\Tools::SaveConfig(true) ){ + return false; + } + + + if( isset($_GET['gpreq']) && $_GET['gpreq'] == 'json' ){ + msg($langmessage['SAVED'].' '.$langmessage['REFRESH']); + }else{ + msg($langmessage['SAVED']); + } + + //resize thumbnails + if( + $config_before['preserve_icc_profiles'] !== $config['preserve_icc_profiles'] + || $config_before['preserve_image_metadata'] !== $config['preserve_image_metadata'] + || $config_before['maxthumbsize'] !== $config['maxthumbsize'] + || $config_before['maxthumbheight'] !== $config['maxthumbheight'] + || $config_before['thumbskeepaspect'] !== $config['thumbskeepaspect'] + ){ + msg(\gp\tool::Link( + 'Admin/Configuration', + $langmessage['recreate_all_thumbnails'], + 'cmd=recreate_thumbs', + [ + 'class' => '', + 'data-cmd' => 'creq', + ] + )); + } + + + } + + + private function getValues(){ + global $config; + + $mailer = new \gp\tool\Emailer(); + + if( $_SERVER['REQUEST_METHOD'] != 'POST'){ + $show = $config; + }else{ + $show = $_POST; + } + if( empty($show['recaptcha_language']) ){ + $show['recaptcha_language'] = 'inherit'; + } + + if( empty($show['from_address']) ){ + $show['from_address'] = $mailer->From_Address(); + } + if( empty($show['from_name']) ){ + $show['from_name'] = $mailer->From_Name(); + } + if( empty($show['mail_method']) ){ + $show['mail_method'] = $mailer->Mail_Method(); + } + + //suhosin will stop the script if a POST value contains a real path like /usr/sbin/sendmail + //if( empty($show['sendmail_path']) ){ + // $show['sendmail_path'] = $mailer->Sendmail_Path(); + //} + + return $show; + } + + + /** + * Get possible configuration values + * + */ + protected function getPossible(){ + global $dataDir,$langmessage; + + $possible = $this->variables; + + $langDir = $dataDir.'/include/thirdparty/ckeditor/lang'; //ckeditor + + $possible['langeditor'] = \gp\tool\Files::readDir($langDir,'js'); + unset($possible['langeditor']['_languages']); + $possible['langeditor']['inherit'] = ' '.$langmessage['default']; //want it to be the first in the list + asort($possible['langeditor']); + + + //recaptcha language + $possible['recaptcha_language'] = []; + $possible['recaptcha_language']['inherit'] = $langmessage['default']; + + // According to https://developers.google.com/recaptcha/docs/language + $recaptcha_languages = [ + 'af', 'am', 'ar', 'az', + 'bn', 'bg', + 'ca', 'cs', + 'da', 'de', 'de-AT', 'de-CH', + 'el', 'en', 'en-GB', 'es', 'es-419', 'et', 'eu', + 'fa', 'fi', 'fil', 'fr', 'fr-CA', + 'gl', 'gu', + 'hi', 'hr', 'hu', 'hy', + 'id', 'is', 'it', 'iw', + 'ja', + 'ka', 'kn', 'ko', + 'lo', 'lt', 'lv', + 'ml', 'mn', 'mr', 'ms', + 'nl', 'no', + 'pl', 'pt', 'pt-BR', 'pt-PT', + 'ro', 'ru', + 'si', 'sk', 'sl', 'sr', 'sv', 'sw', + 'ta', 'te', 'th', 'tr', + 'uk', 'ur', + 'vi', + 'zh-HK', 'zh-CN', 'zh-TW', 'zu', + ]; + foreach($recaptcha_languages as $lang){ + $possible['recaptcha_language'][$lang] = $lang; + } + + //website language + $possible['language'] = $this->GetPossibleLanguages(); + + //tidy + if( function_exists('tidy_parse_string') ){ + $possible['HTML_Tidy'] = [ + 'off' => $langmessage['Off'], + '' => $langmessage['On'] + ]; + }else{ + $possible['HTML_Tidy'] = [''=>'Unavailable']; + } + + //required email fields + $possible['require_email'] = [ + 'none' => 'None', + '' => 'Subject & Message', + 'email' => 'Subject, Message & Email' + ]; + + + //see xoopsmultimailer.php + $possible['mail_method'] = [ + 'mail' => 'PHP mail()', + 'sendmail' => 'sendmail', + 'smtp' => 'smtp', + 'smtpauth' => 'SMTPAuth' + ]; + + //CDN + foreach(\gp\tool\Output\Combine::$scripts as $key => $script_info){ + if( !isset($script_info['cdn']) ){ + continue; + } + + $config_key = 'cdn_'.$key; + + if( !array_key_exists($config_key, $possible) ){ + continue; + } + + $opts = array_keys($script_info['cdn']); + $possible[$config_key] = array_combine($opts, $opts); + array_unshift($possible[$config_key],$langmessage['None']); + } + + gpSettingsOverride('configuration',$possible); + + return $possible; + } + + + /** + * Return a list of possible languages + * Based on the files in /include/languages + * + */ + private function GetPossibleLanguages(){ + global $dataDir; + $lang_dir = $dataDir.'/include/languages'; + + $files = scandir($lang_dir); + $languages = array(); + foreach($files as $file){ + if( $file == '.' || $file == '..' || strpos($file,'main.inc') === false ){ + continue; + } + + $languages[] = str_replace('.main.inc','',$file); + } + + return array_combine($languages, $languages); + } + + + /** + * Display configuration settings + * + */ + protected function showForm(){ + global $langmessage; + + $possible_values = $this->getPossible(); + $array = $this->getValues(); + + echo '
    '; + + + + //order by the possible values + $opened = false; + foreach($possible_values as $key => $possible_value){ + + if( $possible_value === false ){ + if( $opened ){ + echo ''; + $this->SaveAllButton(false); + echo '
    '; + } + echo '

    '; + if( isset($langmessage[$key]) ){ + echo $langmessage[$key]; + }else{ + echo str_replace('_',' ',$key); + } + echo '

    '; + echo ''; + $opened = true; + continue; + } + + if( isset($array[$key]) ){ + $value = $array[$key]; + }else{ + $value = ''; + } + + echo "\n\n"; + + $tr_class_attr = ''; + if( !is_array($possible_value) && $possible_value == 'hidden' ){ + $tr_class_attr = ' class="nodisplay"'; + } + echo ''; + + } + + echo '
    '; + if( isset($langmessage[$key]) ){ + echo $langmessage[$key]; + }else{ + echo str_replace('_',' ',$key); + } + echo ''; + + + if( is_array($possible_value) ){ + self::formSelect($key,$possible_value,$value); + }else{ + switch($possible_value){ + case 'boolean': + $this->formCheckbox($key,$value); + break; + case 'textarea': + $this->formTextarea($key,$value); + break; + default: + $this->formInput($key,$value,$possible_value); + break; + } + } + + if( isset($langmessage['about_config'][$key]) ){ + echo $langmessage['about_config'][$key]; + } + echo '
    '; + + + $this->SaveAllButton(true); + echo '
    '; + } + + + /** + * Display Save buttons + * + */ + protected function SaveButtons(){ + global $langmessage; + + + echo '
    '; + echo ''; + + if( isset($_GET['gpreq']) && $_GET['gpreq'] == 'json' ){ + echo ''; + }else{ + echo ''; + } + + echo '
    '; + + echo '

    '; + echo ''; + echo $langmessage['see_also']; + echo ' '; + echo \gp\tool::Link('Admin/Preferences',$langmessage['Preferences'],'','data-cmd="gpabox"'); + echo '

    '; + + } + + + /** + * Display Save All buttons + * @param boolean $is_last If true include admin notice and hidden cmd input + */ + protected function SaveAllButton($is_last=true){ + global $langmessage; + + echo '
    '; + + if( $is_last ){ + echo ''; + } + + if( isset($_GET['gpreq']) && $_GET['gpreq'] == 'json' ){ + echo ''; + }else{ + echo ''; + } + + echo '
    '; + + if( $is_last ){ + echo '

    '; + echo ''; + echo $langmessage['see_also']; + echo ' '; + echo \gp\tool::Link('Admin/Preferences',$langmessage['Preferences'],'','data-cmd="gpabox"'); + echo '

    '; + } + + } + + + + /** + * Form Functions + * + */ + + public function formCheckbox($key,$value){ + $checked = ''; + if( $value && $value !== 'false' ){ + $checked = ' checked="checked"'; + } + echo '  '; + echo '  '; + } + + public function formInput($name,$value,$type='text'){ + echo "\n
    "; + echo ''; + echo '
    '; + } + + public function formTextarea($name,$value){ + global $langmessage; + $count_label = sprintf($langmessage['_characters'],''.strlen($value).''); + echo ''; + echo ''; + echo ''.$count_label.''; + echo ''; + } + + public static function formSelect($name,$possible,$value=null){ + global $languages; + + foreach($possible as $key => &$val){ + if( isset($languages[$val]) ){ + $val = $languages[$val].' ('.$val.')'; + } + } + + echo \gp\tool\HTML::Select( $possible, $value, ' name="'.$name.'" class="gpselect"'); + } + + + /** + * Recreate all of the thumbnails according to the size in the configuration + * TODO: With lots of images, this may easily exceed PHP script runtime or available memory + * and should be broken up into consecutive AJAX calls + */ + function RecreateThumbs($dir_rel = ''){ + global $dataDir; + + $dir_full = $dataDir.'/data/_uploaded'.$dir_rel; + $files = scandir($dir_full); + + foreach($files as $file){ + + if( $file == '.' || $file == '..' || $file == 'thumbnails' ){ + continue; + } + + $file_full = $dir_full.'/'.$file; + $file_rel = $dir_rel.'/'.$file; + + if( is_dir($file_full) ){ + $this->RecreateThumbs($file_rel); + continue; + } + + if( \gp\admin\Content\Uploaded::IsImg($file_full) ){ + \gp\admin\Content\Uploaded::CreateThumbnail($file_full); + } + } + + } + +} diff --git a/include/admin/Configuration/CDN.php b/include/admin/Configuration/CDN.php new file mode 100644 index 0000000..a731957 --- /dev/null +++ b/include/admin/Configuration/CDN.php @@ -0,0 +1,120 @@ +variables = array( + 'CDN' => false, + 'cdn' + ); + + } + + public function RunScript(){ + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'save_config': + $this->SaveConfig(); + break; + } + + $this->showForm(); + } + + + /** + * Get possible cdn values + * + */ + protected function getPossible(){ + global $langmessage; + + $possible = array(); + $possible['cdn'] = array(); + + foreach(\gp\tool\Output\Combine::$scripts as $key => $script_info){ + + if( !isset($script_info['cdn']) ){ + continue; + } + + foreach($script_info['cdn'] as $cdn => $url){ + $possible['cdn'][] = $cdn; + } + } + $possible['cdn'] = array_combine($possible['cdn'],$possible['cdn']); + $possible['cdn'][''] = $langmessage['None']; + + return $possible; + } + + + /** + * Show CDN Options + * + */ + protected function ShowForm(){ + global $config; + + $possible = $this->getPossible(); + + echo '
    '; + echo '

    CDN

    '; + + + echo ''; + foreach($possible['cdn'] as $cdn_val => $cdn){ + + $checked = ( $cdn_val === $config['cdn'] ) ? 'checked' : ''; + + + echo ''; + } + echo ''; + + + //display which scripts can be served bythe cdn + foreach(\gp\tool\Output\Combine::$scripts as $key => $script_info){ + + if( !isset($script_info['cdn']) || !isset($script_info['label']) ){ + continue; + } + + $code = '\\gp\\tool::LoadComponents(\''.$key.'\');'; + + echo ''; + + foreach($possible['cdn'] as $cdn){ + echo ''; + } + + echo ''; + } + + + echo '
    '; + echo ' '; + echo '
    '; + echo $script_info['label']; + echo ''; + if( isset($script_info['cdn'][$cdn]) ){ + echo ''; + } + echo '
    '; + $this->SaveButtons(); + echo '
    '; + } + +} diff --git a/include/admin/Content/Browser.php b/include/admin/Content/Browser.php new file mode 100644 index 0000000..6a1ed5c --- /dev/null +++ b/include/admin/Content/Browser.php @@ -0,0 +1,35 @@ + 'body'); //force showing only the body as a complete html document + $this->page->get_theme_css = false; + + $this->page->head .= ''; + + $this->Finder(); + } + + function FinderPrep(){ + $this->finder_opts['url'] = \gp\tool::GetUrl('Admin_Finder'); + $this->finder_opts['getFileCallback'] = true; + $this->finder_opts['resizable'] = false; + } + + } +} + +namespace{ + class admin_browser extends \gp\admin\Content\Browser{} +} diff --git a/include/admin/Content/Extra.php b/include/admin/Content/Extra.php new file mode 100644 index 0000000..12790e5 --- /dev/null +++ b/include/admin/Content/Extra.php @@ -0,0 +1,576 @@ +page = $args['page']; + } + + $this->folder = $dataDir . '/data/_extra'; + + if( !empty($args['path_parts']) ){ + $this->extra_part = $args['path_parts'][0]; + } + + $this->SetVars(); + } + + + public function RunScript(){ + + // area specific commands + if( !is_null($this->file) ){ + + $this->cmds['PublishAjax'] = ''; + $this->cmds['EditExtra'] = ''; + $this->cmds['PreviewText'] = ''; + $this->cmds['EditVisibility'] = ''; + $this->cmds['PublishDraft'] = 'Redirect'; + $this->cmds['DismissDraft'] = 'Redirect'; + + + $this->cmds_post['SaveText'] = 'Redirect'; + $this->cmds_post['SaveVisibilityExtra'] = 'Redirect'; + $this->cmds_post['DeleteArea'] = 'DefaultDisplay'; + + + // inline editing + $this->cmds['save'] = 'SectionEdit'; + $this->cmds['save_inline'] = 'SectionEdit'; + $this->cmds['preview'] = 'SectionEdit'; + $this->cmds['include_dialog'] = 'SectionEdit'; + $this->cmds['InlineEdit'] = 'SectionEdit'; + } + + + $this->cmds['gallery_folder'] = 'GalleryImages'; + $this->cmds['gallery_images'] = 'GalleryImages'; + $this->cmds['new_dir'] = '\\gp\\tool\\Editing::NewDirForm'; + $this->cmds['Image_Editor'] = '\\gp\\tool\\Editing::ImageEditor'; + + $this->cmds['NewSection'] = 'DefaultDisplay'; + + $cmd = \gp\tool::GetCommand(); + $this->RunCommands($cmd); + } + + + + /** + * Get a list of all extra edit areas + * + */ + public function SetVars(){ + global $langmessage; + + $this->GetAreas(); + + if( !$this->extra_part ){ + return; + } + + + // is there a specific file being requested + $area_info = $this->ExtraExists($this->extra_part); + + if( is_null($area_info) ){ + msg($langmessage['OOPS'] . ' (Invalid File)'); + return; + } + + $this->area_info = $area_info; + $this->file = $area_info['file_path']; + $this->title = \gp\tool\Editing::CleanTitle($area_info['title']); + $this->draft_file = $area_info['draft_path']; + + $this->file_sections = \gp\tool\Output\Extra::ExtraContent($this->title); + $this->meta_data = \gp\tool\Files::$last_meta; + $this->fileModTime = \gp\tool\Files::$last_modified; + $this->file_stats = \gp\tool\Files::$last_stats; + + $this->vis = \gp\tool\Files::Get('_extra/' . $this->title . '/visibility', 'data'); + $this->vis += ['visibility_type'=>'0','pages'=>[]]; + + $this->draft_exists = \gp\tool\Files::Exists($this->draft_file); + + } + + + + /** + * Get a list of all extra edit areas + * + */ + public function GetAreas(){ + + $this->areas = []; + $files = scandir($this->folder); + + foreach( $files as $file ){ + + $title = self::AreaExists($file); + + if( $title === false ){ + continue; + } + + $this->areas[$title] = [ + 'title' => $title, + 'file_path' => \gp\tool\Files::FilePath($this->folder . '/' . $title . '/page.php'), + 'draft_path' => \gp\tool\Files::FilePath($this->folder . '/' . $title . '/draft.php'), + 'legacy_path' => \gp\tool\Files::FilePath($this->folder . '/' . $title . '.php'), + ]; + + } + + uksort($this->areas, 'strnatcasecmp'); + } + + + /** + * Return the area name if valid + * + */ + public static function AreaExists($title){ + global $dataDir; + + if ($title == '.' || $title == '..'){ + return false; + } + + $legacy = $dataDir . '/data/_extra/' . $title; + $new = $dataDir . '/data/_extra/' . $title . '/page.php'; + + + if( is_dir($legacy) && \gp\tool\Files::Exists($new) ){ //is_dir() used to prevent open_basedir notice http://www.typesettercms.com/Forum?show=t2110 + return $title; + } + + if( substr($title, -4) === '.php' ){ + return substr($title, 0, -4); + } + + return false; + } + + + + /** + * Delete an extra content area + * + */ + public function DeleteArea(){ + global $langmessage; + + //legacy path + if( \gp\tool\Files::Exists($this->area_info['legacy_path']) && !unlink($this->area_info['legacy_path']) ){ + msg($langmessage['OOPS']); + return false; + } + + //remove directory + $dir = dirname($this->area_info['draft_path']); + if( file_exists($dir) && !\gp\tool\Files::RmAll($dir) ){ + msg($langmessage['OOPS']); + return false; + } + + unset($this->areas[$this->title]); + + return true; + } + + + + /** + * Check to see if the extra area exists + * + */ + public function ExtraExists($file){ + + if( !isset($this->areas[$file]) ){ + return; + } + + return $this->areas[$file]; + } + + + + /** + * Show all available extra content areas + * + */ + public function DefaultDisplay(){ + global $langmessage; + + $types = \gp\tool\Output\Sections::GetTypes(); + + echo '

    ' . $langmessage['theme_content'] . '

    '; + echo ''; + echo ''; + echo ''; + + foreach ($this->areas as $file => $info) { + $this->ExtraRow($info, $types); + } + + echo ''; + echo '
    '; + echo $langmessage['file_name']; + echo ''; + echo $langmessage['Content Type']; + echo ' '; + echo $langmessage['options']; + echo '
    '; + + $this->NewExtraForm(); + } + + + + /** + * Display extra content row + * + */ + public function ExtraRow($info, $types){ + global $langmessage; + + $sections = \gp\tool\Output\Extra::ExtraContent($info['title']); + $section = $sections[0]; + + echo ''; + echo str_replace('_', ' ', $info['title']); + echo ''; + $type = $section['type']; + if (isset($types[$type]) && isset($types[$type]['label'])){ + $type = $types[$type]['label']; + } + echo $type; + echo '"'; + $content = strip_tags($section['content']); + echo substr($content, 0, 50); + echo '..."'; + + //preview + echo \gp\tool::Link('Admin/Extra/'.rawurlencode($info['title']), $langmessage['preview'], 'cmd=PreviewText'); + echo '   '; + + //publish & dismiss + if (\gp\tool\Files::Exists($info['draft_path'])){ + echo \gp\tool::Link('Admin/Extra/' . rawurlencode($info['title']), $langmessage['Publish Draft'], 'cmd=PublishDraft', array('data-cmd' => 'post')); + echo '   '; + echo \gp\tool::Link('Admin/Extra/' . rawurlencode($info['title']), $langmessage['Dismiss Draft'], 'cmd=DismissDraft', array('data-cmd' => 'post')); + } else { + echo '' . $langmessage['Publish Draft'] . ''; + echo '   '; + echo '' . $langmessage['Dismiss Draft'] . ''; + } + + echo '   '; + + //edit + if ($section['type'] == 'text'){ + echo \gp\tool::Link('Admin/Extra/' . rawurlencode($info['title']), $langmessage['edit'], 'cmd=EditExtra'); + } else { + echo '' . $langmessage['edit'] . ''; + } + echo '   '; + + //visibility + echo \gp\tool::Link('Admin/Extra/' . rawurlencode($info['title']), $langmessage['Visibility'], 'cmd=EditVisibility'); + echo '   '; + + $title = sprintf($langmessage['generic_delete_confirm'], htmlspecialchars($info['title'])); + echo \gp\tool::Link('Admin/Extra/' . rawurlencode($info['title']), $langmessage['delete'], 'cmd=DeleteArea', array( + 'data-cmd' => 'postlink', + 'title' => $title, + 'class' => 'gpconfirm')); + echo ''; + } + + + + /** + * Display form for defining a new extra edit area + * + */ + public function NewExtraForm(){ + global $langmessage; + + $types = \gp\tool\Output\Sections::GetTypes(); + $_types = []; + foreach( $types as $type => $info ){ + $_types[$type] = $info['label']; + } + + echo '

    '; + echo '

    '; + echo ''; + echo ' '; + + echo \gp\tool\HTML::Select( $_types, key($_types), ' name="type" class="gpselect"'); + + echo ''; + echo '
    '; + echo '

    '; + } + + + public function EditExtra(){ + global $langmessage, $page; + + + $action = \gp\tool::GetUrl('Admin/Extra/' . rawurlencode($this->title), 'cmd=EditExtra'); + $page->head_js[] = '/include/js/admin/extra_edit.js'; + + echo '

    '; + echo \gp\tool::Link('Admin/Extra', $langmessage['theme_content']); + echo ' » ' . str_replace('_', ' ', $this->title) . '

    '; + + echo '
    '; + echo ''; + + \gp\tool\Editing::UseCK($this->file_sections[0]['content']); + + echo ''; + + if( $this->draft_exists ){ + echo ''; + echo ''; + } + + echo \gp\tool::Link('Admin/Extra', $langmessage['Close'], '', array('class' => 'gpcancel')); + + echo '
    '; + } + + + public function SaveText(){ + global $langmessage; + $_POST['cmd'] = 'save_inline'; + if( $this->SectionEdit() ){ + msg($langmessage['SAVED']); + } + } + + + /** + * Preview + * + */ + public function PreviewText(){ + global $langmessage; + + echo '

    '; + echo \gp\tool::Link('Admin/Extra', $langmessage['theme_content']); + echo ' » ' . str_replace('_', ' ', $this->title); + echo '

    '; + echo '
    '; + + $section_num = 0; + \gp\tool\Output\Sections::SetVars('',$this->file_stats); + echo \gp\tool\Output\Sections::GetSection($this->file_sections, $section_num); + echo '
    '; + } + + + /** + * Create a new extra content section + * + */ + public function NewSection(){ + global $langmessage, $gpAdmin; + + $title = str_replace(['\\','/'], '', $_REQUEST['new_title']); + $title = \gp\tool\Editing::CleanTitle($title); + + if (empty($title)){ + msg($langmessage['OOPS'] . ' (Invalid Title)'); + return false; + } + + $types = \gp\tool\Output\Sections::GetTypes(); + $type = htmlspecialchars($_POST['type']); + + if (!array_key_exists($type, $types)){ + msg($langmessage['OOPS'] . ' (Invalid Type)'); + return false; + } + + $file = $this->folder . '/' . $title . '/page.php'; + + $section = \gp\tool\Editing::DefaultContent($type); + $section['created'] = time(); + $section['created_by'] = $gpAdmin['username']; + + $sections = array($section); + + + if (!\gp\tool\Files::SaveData($file, 'file_sections', $sections)){ + msg($langmessage['OOPS'] . ' (Not Saved)'); + return false; + } + + + msg($langmessage['SAVED']); + + $this->GetAreas(); + } + + + /** + * Perform various section editing commands + * + */ + public function SectionEdit(){ + global $langmessage; + + $this->page->file_sections =& $this->file_sections; //hack so the SaveSection filter works + $_REQUEST['section'] = 0; + + return parent::SectionEdit(); + } + + + public function SaveBackup(){ + } + + public function GalleryEdited(){ + } + + public function ResetFileTypes(){ + } + + public function DismissDraft(){ + global $page; + if( \gp\tool\Files::Exists($this->draft_file) && unlink($this->draft_file) ){ + $this->draft_exists = false; + } + + $page->ajaxReplace = array(); + $page->ajaxReplace[] = array('DraftDismissed'); + + return !$this->draft_exists; + } + + + public function EditVisibility(){ + global $langmessage, $page, $gp_index, $gp_titles; + + $action = \gp\tool::GetUrl('Admin/Extra/' . rawurlencode($this->title), 'cmd=EditVisibility'); + $page->head_js[] = '/include/thirdparty/tablesorter/tablesorter.js'; + $page->head_js[] = '/include/js/admin/extra_visibility.js'; + + echo '

    '; + echo \gp\tool::Link('Admin/Extra', $langmessage['theme_content']); + echo ' » ' . str_replace('_', ' ', $this->title); + echo ' » ' . $langmessage['Visibility'] . '

    '; + + echo '
    '; + echo ''; + + echo '

    '; + echo $langmessage['Visibility'] . ':   '; + + $sel_dat = array( + '0' => $langmessage['Show on all pages'], + '1' => $langmessage['Hide on all pages'], + '2' => $langmessage['Show only on selected pages'], + '3' => $langmessage['Hide on selected pages'], + ); + + echo \gp\tool\HTML::Select( $sel_dat, $this->vis['visibility_type'], ' name="visibility_type" id="vis_type" class="gpselect"'); + echo '

    '; + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach( $gp_index as $title => $index ){ + echo ' '; + } + + echo '
    ' . $langmessage["Pages"] . '
    '; + + $check = ''; + if( array_key_exists($index, $this->vis['pages']) ){ + $check = 'checked'; + } + + echo ''; + echo ''; + + $label = \gp\tool::GetLabelIndex($index); + echo '' . $label . ''; + echo '
    '; + echo '
    '; + echo '
    '; + echo '

    '; + echo ''; + echo \gp\tool::Link('Admin/Extra', $langmessage['Close'], '', array('class' => 'gpcancel')); + echo '

    '; + echo '
    '; + + } + + + /** + * Save extra area visibility + * + */ + public function SaveVisibilityExtra(){ + global $langmessage, $gp_titles; + + $file = '_extra/' . $this->title . '/visibility'; + $data = []; + $data['visibility_type'] = $_REQUEST['visibility_type']; + + if( isset($_REQUEST['pages']) && is_array($_REQUEST['pages']) ){ + $data['pages'] = array_intersect_key($_REQUEST['pages'], $gp_titles); + } + + if( !\gp\tool\Files::SaveData($file, 'data', $data) ){ + msg($langmessage['OOPS']); + return false; + } + + msg($langmessage['SAVED']); + return true; + } + + + /** + * Redirect the user request + * + */ + public function Redirect(){ + + $req_type = \gp\tool::RequestType(); + + if( $req_type != 'json' ){ + \gp\tool::Redirect(['Admin/Extra',$_GET]); + } + } + +} diff --git a/include/admin/Content/Galleries.php b/include/admin/Content/Galleries.php new file mode 100644 index 0000000..183135b --- /dev/null +++ b/include/admin/Content/Galleries.php @@ -0,0 +1,179 @@ +galleries = self::GetData(); + $this->page = $args['page']; + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + + case 'newdrag': + $this->NewDrag(); + return; + } + + $this->EditGalleries(); + } + + public function EditGalleries(){ + global $langmessage; + + $this->page->head_js[] = '/include/js/special_galleries.js'; + $this->page->css_admin[] = '/include/css/edit_gallery.css'; + + + + echo '

    '; + echo \gp\tool::Link('Special_Galleries',\gp\tool\Output::ReturnText('galleries')); + echo ' » '.$langmessage['administration']; + echo '

    '; + + echo '

    '; + echo $langmessage['DRAG-N-DROP-DESC2']; + echo '

    '; + + $this->EditableArea(); + + } + + + + public function EditableArea(){ + global $gp_titles, $gp_index, $langmessage; + + + echo ''; + + echo ''; + + echo '
    '; + echo '

    '.$langmessage['visible_galleries'].'

    '; + echo '
    '; + echo '

    '.$langmessage['hidden_galleries'].'

    '; + echo '
    '; + echo '
    '; + + foreach($this->galleries as $title => $info ){ + + if( !$this->GalleryVisible($title,$info) ){ + continue; + } + + $this->GalleryEditBox( $title, $info ); + } + + + echo '
    '; + echo '
    '; + + + echo '
    '; + if( count($this->not_visible) > 0 ){ + foreach($this->not_visible as $title => $info){ + $this->GalleryEditBox( $title, $info ); + } + } + + + echo '
    '; + + echo '
    '; + + } + + + public function GalleryEditBox( $title, $info ){ + if( is_array($info) ){ + $icon = $info['icon']; + }else{ + $icon = $info; + } + + if( empty($icon) ){ + $thumbPath = \gp\tool::GetDir('/include/imgs/blank.gif'); + }elseif( strpos($icon,'/thumbnails/') === false ){ + $thumbPath = \gp\tool::GetDir('/data/_uploaded/image/thumbnails'.$icon.'.jpg'); + }else{ + $thumbPath = \gp\tool::GetDir('/data/_uploaded'.$icon); + } + echo '
    '; + echo \gp\tool::Link('Special_Galleries',htmlspecialchars($title),'cmd=drag&to=%s&title='.urlencode($title),'data-cmd="gpajax" class="dragdroplink nodisplay" '); + echo ''; + + echo ' '; + echo '
    '; + echo str_replace('_',' ',$title); + echo '
    '; + echo '
    '; + } + + + public function NewDrag(){ + global $langmessage, $gp_index, $gp_titles; + $this->page->ajaxReplace = array(); + + + //get the title of the gallery that was moved + $dragging = \gp\tool::ArrayKey( $_POST['title'], $this->galleries, '(Title not in gallery list)' ); + if( !$dragging ){ + return false; + } + + $index = $gp_index[$dragging]; + $info = $this->galleries[$dragging]; + unset($this->galleries[$dragging]); + + + //set visibility + if( isset($_POST['active']) ){ + $info['visibility'] = 'show'; + unset($gp_titles[$index]['vis']); + }else{ + $info['visibility'] = 'hide'; + } + + + //place before the element represented by $_POST['next'] if it's set + if( isset($_POST['next']) ){ + + $next = \gp\tool::ArrayKey( $_POST['next'], $this->galleries, '(Next not found)' ); + if( !$next ){ + return false; + } + + if( !\gp\tool\Files::ArrayInsert($next,$dragging,$info,$this->galleries) ){ + msg($langmessage['OOPS'].' (Insert Failed)'); + return false; + } + + //place at the end + }else{ + $this->galleries[$dragging] = $info; + } + + //save it + if( !self::SaveIndex($this->galleries) ){ + msg($langmessage['OOPS'].' (Not Saved)'); + return false; + } + + if( !\gp\admin\Tools::SavePagesPHP(true) ){ + return false; + } + + + + } + + + +} diff --git a/include/admin/Content/Revisions.php b/include/admin/Content/Revisions.php new file mode 100644 index 0000000..681a235 --- /dev/null +++ b/include/admin/Content/Revisions.php @@ -0,0 +1,336 @@ + 'DefaultDisplay', + 'UseRevision' => 'DefaultDisplay', + ]; + + + public function __construct($args){ + global $gp_index, $gpAdmin; + + //parent::__construct($args); + + if( empty($args['path_parts']) ){ + $url = \gp\tool::GetUrl('Admin'); + \gp\tool::Redirect($url); + } + + $index = $args['path_parts'][0]; + $title = array_search($index, $gp_index); + + if( $title === false ){ + $url = \gp\tool::GetUrl('Admin'); + \gp\tool::Redirect($url); + } + + if ( isset($gpAdmin['locked']) && $gpAdmin['locked'] ){ + $url = \gp\tool::GetUrl($title); + \gp\tool::Redirect($url); + } + + parent::__construct($title,''); + + } + + public function RunScript(){ + + $this->SetVars(); + $this->GetFile(); + + $cmd = \gp\tool::GetCommand(); + $this->RunCommands($cmd); + } + + + public function DefaultDisplay(){ + global $page, $langmessage; + + + $page->head_js[] = '/include/js/admin/revisions.js'; + + //show site in iframe + $url = \gp\tool::GetUrl($this->title,'cmd=ViewRevision&revision=draft'); + $toolbar = '

    ' . $langmessage['Revision History'] . '

    '; + // $toolbar .= \gp\tool::Link_Page($this->title); + + $toolbar .= '
    '; + + $toolbar .= ''; + // $toolbar .= '   '; + $toolbar .= ''; + + $toolbar .= \gp\tool::Link( + $this->title, + $langmessage['Close'], + '', + array('class' => 'gpbutton revision_history_close') + ); + + $toolbar .= '
    '; + + + + + ob_start(); + $this->ViewHistory(); + $content = ob_get_clean(); + + + \gp\admin\Tools\Iframe::Output( $page, $url, $toolbar, $content); + } + + + /** + * Display the revision history of the current file + * + */ + public function ViewHistory(){ + global $langmessage, $config; + + $files = $this->BackupFiles(); + $rows = array(); + + foreach($files as $time => $file){ + $info = $this->BackupInfo($file); + $rows[] = [ + 'time' => $time, + 'row' => $this->HistoryRow($info['time'], $info['size'], $info['username']) + ]; + } + + // current page + // this will overwrite one of the history entries if there is a draft + $page_file = \gp\tool\Files::FilePath($this->file); + $rows[] = [ + 'time' => $this->fileModTime, + 'row' => $this->HistoryRow($this->fileModTime, filesize($page_file), $this->file_stats['username'], 'current') + ]; + + usort($rows,function($a,$b){ + return strnatcmp($b['time'],$a['time']); + }); + + // working draft + // always make it the first row + if( $this->draft_exists ){ + $draft_file = \gp\tool\Files::FilePath($this->draft_file); + $size = filesize($draft_file); + $time = $this->file_stats['modified']; + + array_unshift($rows,['time'=>$time,'row'=>$this->HistoryRow($time, $size, $this->file_stats['username'], 'draft')]); + } + + + // echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach($rows as $row){ + echo $row['row']; + } + + echo ''; + echo '
    ' . $langmessage['Modified'] . '' . $langmessage['File Size'] . '' . $langmessage['username'] . '' . $langmessage['options'] . '
    '; + + echo '

    ' . $langmessage['history_limit'] . ': ' . $config['history_limit'] . '

    '; + } + + + + /** + * Return content for history row + * + */ + protected function HistoryRow($time, $size, $username, $which='history'){ + global $langmessage; + static $i = 1; + + ob_start(); + $date = \gp\tool::date($langmessage['strftime_datetime'], $time); + echo ''; + switch($which){ + case 'current': + echo '' . $langmessage['Current Page'] . '
    '; + break; + + case 'draft': + echo '' . $langmessage['Working Draft'] . '
    '; + break; + } + + $elapsed = \gp\admin\Tools::Elapsed(time() - $time); + echo sprintf($langmessage['_ago'], $elapsed); + echo ''; + if( $size && is_numeric($size) ){ + echo \gp\admin\Tools::FormatBytes($size); + } + echo ''; + if( !empty($username) ){ + echo $username; + } + echo ''; + + + switch($which){ + case 'current': + echo \gp\tool::Link( + $this->title, + $langmessage['View'], + 'cmd=ViewCurrent', + [ + 'target' => 'gp_layout_iframe' + ] + ); + if( $this->draft_exists ){ + echo \gp\tool::Link( + 'Admin/Revisions/' . $this->gp_index, + $langmessage['restore'], + 'cmd=UseRevision&revision=current', + [ + 'data-cmd' => 'post', + 'class' => 'msg_publish_draft admin-link admin-link-publish-draft' + ] + ); + }else{ + echo \gp\tool::Link( + $this->title, + $langmessage['edit'] + ); + } + break; + + case 'draft': + echo \gp\tool::Link( + $this->title, + $langmessage['View'], + 'cmd=ViewRevision&revision=draft', + [ + 'target' => 'gp_layout_iframe' + ] + ); + + echo \gp\tool::Link( + $this->title, + $langmessage['edit'] + ); + break; + + case 'history': + echo \gp\tool::Link( + $this->title, + $langmessage['View'], + 'cmd=ViewRevision&revision=' . $time, + [ + 'target' => 'gp_layout_iframe', + ] + ); + + echo \gp\tool::Link( + 'Admin/Revisions/' . $this->gp_index, + $langmessage['restore'], + 'cmd=UseRevision&revision=' . $time, + [ + 'data-cmd' => 'post', + 'class' => 'msg_publish_draft admin-link admin-link-publish-draft' + ] + ); + + echo \gp\tool::Link( + '/Admin/Revisions/'.$this->gp_index, + '', + 'cmd=DeleteRevision&revision=' . $time, + [ + 'title' => $langmessage['delete'], + 'class' => 'gpconfirm', + 'data-cmd' => 'post', + ] + ); + break; + } + + echo ''; + return ob_get_clean(); + } + + /** + * Delete a revision backup + * + */ + public function DeleteRevision(){ + + $full_path = $this->BackupFile($_REQUEST['revision']); + if( is_null($full_path) ){ + return false; + } + unlink($full_path); + } + + /** + * Revert the file data to a previous revision + * + */ + protected function UseRevision(){ + + $revision =& $_REQUEST['revision']; + + if ($revision == 'current') { + $file_sections = \gp\tool\Files::Get($this->file, 'file_sections'); + } else { + $file_sections = $this->GetRevision($revision); + } + + if( $file_sections === false ){ + return false; + } + + $this->file_sections = $file_sections; + $this->SaveThis(); + + $url = \gp\tool::GetUrl('Admin/Revisions/'.$this->gp_index); + \gp\tool::Redirect($url); + } + + + /** + * Get info about a backup from the filename + * + */ + public function BackupInfo($file){ + + $info = array(); + + //remove .gze + if( strpos($file,'.gze') === (strlen($file)-4) ){ + $file = substr($file, 0, -4); + } + + $name = basename($file); + $parts = explode('.', $name, 3); + + $info['time'] = array_shift($parts); + $info['size'] = array_shift($parts); + $info['username'] = ''; + + if( count($parts) ){ + $info['username'] = array_shift($parts); + } + + return $info; + } + +} diff --git a/include/admin/Content/Trash.php b/include/admin/Content/Trash.php new file mode 100644 index 0000000..a0c9c08 --- /dev/null +++ b/include/admin/Content/Trash.php @@ -0,0 +1,641 @@ +page->head_js[] = '/include/js/admin/trash.js'; + $this->trash_files = self::TrashFiles(); + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + + case 'RestoreDeleted': + $this->RestoreDeleted(); + break; + + case 'DeleteFromTrash': + $this->DeleteFromTrash(); + break; + } + + //view a trash file + $parts = explode('/',$this->page->requested); + if( count($parts) > 2 ){ + $title = $parts[2]; + if( isset($this->trash_files[$title]) ){ + $this->ViewTrashFile($title); + return; + } + } + + //view all trash files + $this->Trash(); + + } + + + /** + * Add/Remove titles from the trash.php index file + * Delete files in $remove_from_trash + * + */ + public static function ModTrashData($add_to_trash,$remove_from_trash){ + + $trash_titles = self::TrashFiles(); + + foreach((array)$remove_from_trash as $title_index => $info){ + unset($trash_titles[$title_index]); + + //only delete the files if they're in the trash directory + if( strpos($info['rm_path'],'/data/_trash') !== false ){ + \gp\tool\Files::RmAll($info['rm_path']); + } + } + + return self::SaveTrashTitles($trash_titles); + } + + + + /** + * Return a sorted array of files in the trash + * + */ + public static function TrashFiles(){ + global $dataDir, $gp_index, $config; + + $trash_titles = array(); + + // pre 4.6, deleted page info was stored + $trash_file = $dataDir.'/data/_site/trash.php'; + if( \gp\tool\Files::Exists($trash_file) ){ + $trash_titles = \gp\tool\Files::Get($trash_file,'trash_titles'); + } + + + // get files associated existing titles + $pages_dir = $dataDir.'/data/_pages/'; + $pages_dir_len = strlen($pages_dir); + $existing = array(); + foreach($gp_index as $title => $index){ + + if( \gp\tool::SpecialOrAdmin($title) !== false ){ + continue; + } + + + $file = \gp\tool\Files::PageFile($title); + $file = substr($file,$pages_dir_len); + + if( strpos($file,'/') ){ + $existing[] = dirname($file); + }else{ + $existing[] = $file; + } + } + + + // post 4.6, deleted pages are left in the data/_pages folder + $files = scandir($pages_dir); + $files = array_diff($files, array('.','..','index.html')); + + + // add the new files to the list of $trash_titles + $new_trash_files = array_diff($files,$existing); + $page_prefix = substr($config['gpuniq'],0,7).'_'; + foreach($new_trash_files as $file){ + + $info = array(); + $info_file = $dataDir.'/data/_pages/'.$file.'/deleted.php'; + if( \gp\tool\Files::Exists($info_file) ){ + $info = \gp\tool\Files::Get($info_file,'deleted'); + $info['page_file'] = $dataDir.'/data/_pages/'.$file.'/page.php'; + }else{ + $info['page_file'] = $dataDir.'/data/_pages/'.$file; + $info['time'] = filemtime($info['page_file']); + $info['orphaned'] = true; + } + + //get index + if( strpos($file,$page_prefix) === 0 ){ + $info['index'] = substr($file,8); // remove page_prefix + } + + $info['rm_path'] = $dataDir.'/data/_pages/'.$file; + $trash_titles[$file] = $info; + } + + //make sure we have a title + foreach($trash_titles as $trash_index => &$info){ + if( !isset($info['title']) ){ + $info['title'] = str_replace('_',' ',$trash_index); + } + } + + uasort($trash_titles,array('self','TitleSort')); + + return $trash_titles; + } + + + public static function TitleSort($a,$b){ + return strnatcasecmp($a['title'],$b['title']); + } + + + /* + * Save $trash_titles to the trash.php index file + * + */ + public static function SaveTrashTitles($trash_titles){ + global $dataDir; + $index_file = $dataDir.'/data/_site/trash.php'; + return \gp\tool\Files::SaveData($index_file,'trash_titles',$trash_titles); + } + + + + + /** + * Get the $info array for $title for use with $gp_titles + * + */ + public static function GetInfo($trash_index){ + global $dataDir; + static $trash_titles = false; + + if( $trash_titles === false ){ + $trash_titles = self::TrashFiles(); + } + + if( !array_key_exists($trash_index,$trash_titles) ){ + return false; + } + + $title_info = $trash_titles[$trash_index]; + + + //make sure we have a file or dir + if( empty($title_info['rm_path']) ){ + + if( empty($title_info['file']) ){ + $title_info['file'] = $trash_index.'.php'; + } + $title_info['rm_path'] = $dataDir.'/data/_trash/'.$title_info['file']; + $title_info['page_file'] = $dataDir.'/data/_trash/'.$title_info['file']; + } + + + + //make sure we have a label + if( empty($title_info['label']) ){ + $title_info['label'] = \gp\admin\Tools::LabelToSlug($trash_index); + } + + //make sure we have a file_type + if( empty($title_info['type']) ){ + $title_info['type'] = self::GetTypes($title_info['page_file']); + } + + + return $title_info; + } + + + + /** + * Copy the php file in _pages to _trash for $title + * + */ + public static function MoveToTrash_File($title, $index, &$trash_data){ + global $dataDir, $gp_titles, $config; + + + //get the file data + $source_file = \gp\tool\Files::PageFile($title); + $source_dir = dirname($source_file); + $trash_file = $source_dir.'/deleted.php'; + $file_sections = \gp\tool\Files::Get($source_file,'file_sections'); + + + //create trash info file + $trash_info = $gp_titles[$index]; + $trash_info['title'] = $title; + $trash_info['time'] = time(); + + if( !\gp\tool\Files::SaveData($trash_file,'deleted',$trash_info) ){ + return false; + } + + + //update image information + if( count($file_sections) ){ + includeFile('image.php'); + \gp_resized::SetIndex(); + foreach($file_sections as $section_data){ + if( isset($section_data['resized_imgs']) ){ + \gp\tool\Editing::ResizedImageUse($section_data['resized_imgs'],array()); + } + } + } + + return true; + } + + + /** + * Remove files from the trash by restoring them to $gp_titles and $gp_index + * + */ + public function RestoreDeleted(){ + global $langmessage,$gp_titles,$gp_index; + + if( empty($_POST['titles']) || !is_array($_POST['titles']) ){ + msg($langmessage['OOPS'].' (No Titles)'); + return; + } + + $titles = $_POST['titles']; + self::RestoreTitles($titles); + + if( !$titles ){ + msg($langmessage['OOPS'].' (R1)'); + return false; + } + + if( !\gp\admin\Tools::SavePagesPHP(true) ){ + return false; + } + + self::ModTrashData(null,$titles); + + $show_titles = array(); + foreach($titles as $trash_index => $info){ + $show_titles[] = \gp\tool::Link($info['title'],$info['title']); + unset($this->trash_files[$trash_index]); + } + $title_string = implode(', ',$show_titles); + + $link = \gp\tool::GetUrl('Admin/Menu'); + $message = sprintf($langmessage['file_restored'],$title_string,$link); + + msg($message); + } + + + + /** + * Restore $titles and return array with menu information + * @param array $titles An array of titles to be restored. After completion, it will contain only the titles that were prepared successfully + * @return array A list of restored titles that can be used for menu insertion + * + */ + public static function RestoreTitles(&$titles){ + global $dataDir, $gp_index, $gp_titles, $config; + + $new_menu = array(); + $restored = array(); + foreach($titles as $trash_index){ + + //get trash info about file + $title_info = self::GetInfo($trash_index); + if( $title_info === false ){ + continue; + } + + $new_title = \gp\admin\Tools::CheckPostedNewPage($title_info['title'],$message); + if( empty($new_title) ){ + continue; + } + + + //make sure the page_file exists + if( !\gp\tool\Files::Exists($title_info['page_file']) ){ + continue; + } + + + //add to $gp_index before PageFile() + if( isset($title_info['index']) ){ + $index = $title_info['index']; + $gp_index[$new_title] = $index; + }else{ + $index = \gp\tool::NewFileIndex(); + $gp_index[$new_title] = $index; + } + + + // move the trash file to the /_pages directory if needed + $new_file = \gp\tool\Files::PageFile($new_title); + if( !\gp\tool\Files::Exists($new_file) ){ + if( !\gp\tool\Files::Rename($title_info['page_file'],$new_file) ){ + unset($gp_index[$new_title]); + continue; + } + } + + + //add to $gp_titles + $gp_titles[$index] = array(); + $gp_titles[$index]['label'] = $title_info['label']; + $gp_titles[$index]['type'] = $title_info['type']; + + $new_menu[$index] = array(); + $restored[$trash_index] = $title_info; + + self::RestoreFile($new_title, $new_file, $title_info); + } + + $titles = $restored; + + return $new_menu; + } + + /** + * Get the content of the file in the trash so we can restore file information + * - resized images + * - \gp\special\Galleries::UpdateGalleryInfo($title,$content) + * + */ + public static function RestoreFile($title,$file,$title_info){ + + $file_sections = \gp\tool\Files::Get($file,'file_sections'); + + // Restore resized images + if( count($file_sections) ){ + includeFile('image.php'); + \gp_resized::SetIndex(); + foreach($file_sections as $section => $section_data){ + + if( !isset($section_data['resized_imgs']) ){ + continue; + } + + foreach($section_data['resized_imgs'] as $image_index => $sizes){ + if( !isset(\gp_resized::$index[$image_index]) ){ + continue; + } + $img = \gp_resized::$index[$image_index]; + foreach($sizes as $size){ + list($width,$height) = explode('x',$size); + \gp\tool\Editing::CreateImage($img,$width,$height); + } + } + \gp\tool\Editing::ResizedImageUse(array(),$section_data['resized_imgs']); + } + \gp_resized::SaveIndex(); + } + + + // Restore Galleries + if( strpos($title_info['type'],'gallery') !== false ){ + \gp\special\Galleries::UpdateGalleryInfo($title,$file_sections); + } + } + + + + /** + * Get the section types so we can set the $gp_titles and $meta_data variables correctly + * In future versions, fetching the $meta_data['file_type'] value will suffice + * + */ + public static function GetTypes($file){ + + $types = array(); + $file_sections = \gp\tool\Files::Get($file,'file_sections'); + + foreach($file_sections as $section){ + $types[] = $section['type']; + } + $types = array_unique($types); + return implode(',',$types); + } + + + /** + * View all files in the trash + * + */ + public function Trash(){ + global $dataDir,$langmessage; + + $this->section_types = \gp\tool\Output\Sections::GetTypes(); + + echo '

    '.$langmessage['trash'].'

    '; + + + if( count($this->trash_files) == 0 ){ + echo '
    • '.$langmessage['TRASH_IS_EMPTY'].'
    '; + return false; + } + + echo '
    '; + echo ''; + + ob_start(); + echo ''; + $heading = ob_get_clean(); + + echo $heading; + + // non-orphaned + $orphaned = array(); + foreach($this->trash_files as $trash_index => $info){ + if( isset($info['orphaned']) ){ + $orphaned[$trash_index] = $info; + }else{ + $this->TrashRow($trash_index, $info); + } + } + + // orphaned files + if( $orphaned ){ + echo ''; + + foreach($orphaned as $trash_index => $info){ + $this->TrashRow($trash_index, $info); + } + } + + + + echo $heading; + + echo '
    '; + echo ''; + echo ''; + echo ' '; + echo ''; + echo '
    '; + echo '   '; + echo count($orphaned).' Orphaned Files Found'; + echo ''; + echo ''.$langmessage['View'].'   '; + $q = array(); + $q['titles'] = array_keys($orphaned); + $q = 'cmd=DeleteFromTrash&'.http_build_query($q); + echo \gp\tool::Link('Admin/Trash',$langmessage['delete'],$q,array('data-cmd'=>'postlink')); + echo '
    '; + echo '
    '; + } + + public function TrashRow($trash_index, $info, $show_orphaned = false ){ + global $langmessage; + + $class = ''; + if( isset($info['orphaned']) ){ + $class = 'orphaned'; + } + + //title + echo ''; + echo ''; + + //time + echo ''; + + if( !empty($info['time']) ){ + $elapsed = \gp\admin\Tools::Elapsed(time() - $info['time']); + echo sprintf($langmessage['_ago'],$elapsed); + } + + echo ''; + if( isset($info['type']) ){ + $this->TitleTypes($info['type']); + } + + echo ''; + + if( \gp\admin\Tools::CheckPostedNewPage($info['title'], $msg) ){ + echo \gp\tool::Link('Admin/Trash',$langmessage['restore'],'cmd=RestoreDeleted&titles[]='.rawurlencode($trash_index),array('data-cmd'=>'postlink')); + }else{ + echo ''.$langmessage['restore'].''; + } + echo '   '; + echo \gp\tool::Link('Admin/Trash',$langmessage['delete'],'cmd=DeleteFromTrash&titles[]='.rawurlencode($trash_index),array('data-cmd'=>'postlink')); + + echo ''; + } + + /** + * List section types + * + */ + public function TitleTypes($types){ + global $gp_titles; + + $types = explode(',',$types); + $types = array_filter($types); + $types = array_unique($types); + + foreach($types as $i => $type){ + if( isset($this->section_types[$type]) && isset($this->section_types[$type]['label']) ){ + $types[$i] = $this->section_types[$type]['label']; + } + } + + echo implode(', ',$types); + } + + /** + * Check and remove the requested files from the trash + * + */ + public function DeleteFromTrash(){ + global $dataDir,$langmessage; + + if( empty($_POST['titles']) || !is_array($_POST['titles']) ){ + msg($langmessage['OOPS'].' (No Titles)'); + return; + } + + $titles = array(); + $incomplete = false; + + foreach($_POST['titles'] as $trash_index){ + $title_info = self::GetInfo($trash_index); + + if( $title_info === false ){ + $incomplete = true; + continue; + } + $titles[$trash_index] = $title_info; + } + + if( !self::ModTrashData(null,$titles) ){ + return false; + } + + + //remove the data + foreach($titles as $trash_index => $info){ + \gp\tool\Files::RmAll($info['rm_path']); + unset($this->trash_files[$trash_index]); + } + + + if( $incomplete ){ + msg($langmessage['delete_incomplete']); + } + } + + + /** + * View the contents of a trash file + * + */ + public function ViewTrashFile($trash_index){ + global $dataDir, $langmessage, $trash_file; + + $title_info = self::GetInfo($trash_index); + + + //delete / restore links + echo '
    '; + echo \gp\tool::Link('Admin/Trash',$langmessage['restore'],'cmd=RestoreDeleted&titles[]='.rawurlencode($trash_index),array('data-cmd'=>'cnreq','class'=>'gpsubmit')); + echo '   '; + echo \gp\tool::Link('Admin/Trash',$langmessage['delete'],'cmd=DeleteFromTrash&titles[]='.rawurlencode($trash_index),array('data-cmd'=>'cnreq','class'=>'gpsubmit')); + echo '
    '; + + + echo '

    '; + echo \gp\tool::Link('Admin/Trash',$langmessage['trash']); + echo ' » '; + echo htmlspecialchars($title_info['title']); + echo '

    '; + echo '
    '; + + + //get file sections + $file_sections = \gp\tool\Files::Get($title_info['page_file'],'file_sections'); + + if( $file_sections ){ + echo \gp\tool\Output\Sections::Render($file_sections,$title_info['title']); + }else{ + echo '

    This page no longer has any content

    '; + } + } + +} + diff --git a/include/admin/Content/Uploaded.php b/include/admin/Content/Uploaded.php new file mode 100644 index 0000000..279fe42 --- /dev/null +++ b/include/admin/Content/Uploaded.php @@ -0,0 +1,1064 @@ +do_admin_uploaded($file_cmd); + }else{ + $this->Finder(); + } + } + + public function Finder(){ + global $config, $dataDir; + + $this->page->head .= "\n".''; + $this->page->css_admin[] = '/include/css/admin_finder.scss'; + + echo '
    '; + + \gp\tool::LoadComponents('selectable,draggable,droppable,resizable,dialog,slider,button'); + + + //get the finder language + $language = $config['langeditor']; + if( $language == 'inherit' ){ + $language = $config['language']; + } + $lang_file = '/include/thirdparty/elFinder/js/i18n/elfinder.'.$language.'.js'; + $lang_full = $dataDir.$lang_file; + if( file_exists($lang_full) ){ + // elFinder 2.3.2 will liad the lang file via require.js + // $this->page->head .= "\n".''; + }else{ + $language = 'en'; + } + $this->finder_opts['lang'] = $language; + $this->finder_opts['customData']['verified'] = \gp\tool\Nonce::Create('post',true); + + + $this->finder_opts['uiOptions'] = array( + + // toolbar configuration + 'toolbar' => array( + array('back', 'forward','up','reload'), + array('home','netmount'), + array('mkdir', 'upload'), //'mkfile', + array('open', 'download', 'getfile'), + array('info'), + array('quicklook'), + array('copy', 'cut', 'paste'), + array('rm'), + array('duplicate', 'rename', 'edit', 'resize'), + array('extract', 'archive'), + array('search'), + array('view','sort'), + array('help') + ), + + // directories tree options + 'tree' => array( + // expand current root on init + 'openRootOnLoad' => true, + // auto load current dir parents + 'syncTree' => true, + ), + + // navbar options + 'navbar' => array( + 'minWidth' => 150, + 'maxWidth' => 500 + ), + + // current working directory options + 'cwd' => array( + // display parent directory in listing as ".." + 'oldSchool' => false + ) + ); + + + $this->FinderPrep(); + + $this->finder_opts = \gp\tool\Plugins::Filter('FinderOptionsClient',array($this->finder_opts)); + gpSettingsOverride('finder_options_client',$this->finder_opts); + + $this->page->head_script .= "\n".'var finder_opts = '.json_encode($this->finder_opts).';'; + } + + public function FinderPrep(){ + $this->finder_opts['url'] = \gp\tool::GetUrl('Admin_Finder'); + $this->finder_opts['height'] = '100%'; + $this->finder_opts['resizable'] = false; + } + + + public function do_admin_uploaded($file_cmd){ + + $this->Init(); + $this->page->ajaxReplace = array(); + + switch($file_cmd){ + case 'delete': + $this->DeleteConfirmed(); + return; + + case 'inline_upload': + $this->InlineUpload(); + //dies + } + } + + public function Init(){ + global $langmessage, $dataDir; + + $this->baseDir = $dataDir.'/data/_uploaded'; + $this->thumbFolder = $dataDir.'/data/_uploaded/image/thumbnails'; + $this->currentDir = $this->baseDir; + $this->page->label = $langmessage['uploaded_files']; + + $this->imgTypes = array('bmp'=>1,'png'=>1,'jpg'=>1,'jpeg'=>1,'gif'=>1,'tiff'=>1,'tif'=>1,'svg'=>1); + + $this->SetDirectory(); + + //prompt to create the requested subdirectory + if( !file_exists($this->currentDir) ){ + \gp\tool\Files::CheckDir($this->currentDir); + } + + + //is in thumbnail directory? + if( strpos($this->currentDir,$this->thumbFolder) !== false ){ + $this->isThumbDir = true; + } + $this->currentDir_Thumb = $this->thumbFolder.$this->subdir; + + } + + /** + * Set the upload directory + * + */ + public function SetDirectory(){ + + $subdir = ''; + $path = \gp\tool::WhichPage(); // get the current path, not using $page->requested since space characters will have been changed to underscores + $path = str_replace('\\','/',$path); + + //@since 5.0 + if( strpos($path,'Admin/Uploaded') === 0 ){ + $path = substr($path,14); + $path = trim($path,'/'); + $parts = explode('/',$path); + + + //backwards compat + }else{ + $path = trim($path,'/'); + $parts = explode('/',$path); + array_shift($parts); + } + + if( count($parts) > 0 ){ + $subdir = '/'.implode('/',$parts); + $subdir = \gp\tool\Editing::CleanArg($subdir); + } + + + if( !empty($_REQUEST['dir']) ){ + $subdir .= \gp\tool\Editing::CleanArg($_REQUEST['dir']); + } + + $subdir = \gp\tool\Files::Canonicalize($subdir); + $subdir = rtrim($subdir,'/'); + $current_dir = $this->currentDir . $subdir; + + if( !\gp\tool\Files::CheckPath( $current_dir, $this->currentDir ) ){ + return; + } + + $this->subdir = $subdir; + $this->currentDir = $current_dir; + } + + + public function ReadableMax(){ + $value = ini_get('upload_max_filesize'); + + if( empty($value) ){ + return '2 Megabytes';//php default + } + return $value; + } + + + public static function Max_File_Size(){ + $value = ini_get('upload_max_filesize'); + if( empty($value) ){ + return; + } + $max = \gp\tool::getByteValue($value); + if( $max !== false ){ + echo ''; + } + } + + + /** + * Upload one image + * + */ + public function InlineUpload(){ + + if( count($_FILES['userfiles']['name']) != 1 ){ + $this->InlineResponse('failed','Empty Array'); + } + + $name = $_FILES['userfiles']['name'][0]; + if( empty($name) ){ + $this->InlineResponse('failed','Empty Name'); + } + + $uploaded = $this->UploadFile(0); + if( $uploaded === false ){ + reset($this->errorMessages); + $this->InlineResponse('failed',current($this->errorMessages)); + } + \gp\tool\Plugins::Action('FileUploaded',$uploaded); + + $return_content = self::ShowFile_Gallery($this->subdir,$uploaded); + + if( is_string($return_content) ){ + $this->InlineResponse('success',$return_content); + }else{ + $this->InlineResponse('notimage',''); + } + + } + + /** + * Output a list a images in a director for use in inline editing + * @static + */ + public static function InlineList($dir_piece){ + global $langmessage, $dataDir, $page; + + $page->ajaxReplace = array(); + + + $dir_piece = \gp\tool::WinPath($dir_piece); + $dir = $dataDir.'/data/_uploaded'.$dir_piece; + + $prev_piece = false; + + while( ($dir_piece != '/') && !file_exists($dir) ){ + $prev_piece = $dir_piece; + $dir = \gp\tool::DirName($dir); + $dir_piece = \gp\tool::DirName($dir_piece); + } + + //new directory? + if( $prev_piece ){ + $prev_piece = \gp\tool\Editing::CleanArg($prev_piece); + $dir_piece = $prev_piece; + $dir = $dataDir.'/data/_uploaded'.$prev_piece; + + if( !\gp\tool\Files::CheckDir($dir) ){ + msg($langmessage['OOPS']); + $dir = \gp\tool::DirName($dir); + $dir_piece = \gp\tool::DirName($prev_piece); + } + } + + + //folder information + $folders = $files = array(); + $allFiles = \gp\tool\Files::ReadFolderAndFiles($dir); + list($folders,$files) = $allFiles; + + //available images + ob_start(); + $image_count = 0; + foreach($files as $file){ + $img = self::ShowFile_Gallery($dir_piece,$file); + if( is_string($img) ){ + echo $img; + $image_count++; + } + } + $gp_gallery_avail_imgs = ob_get_clean(); + + + $gp_option_area = self::InlineList_Options($dir_piece, $folders); + $folder_options = self::InlineList_Folder($image_count, $dir_piece); + + + + //send content according to request + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'gallery_folder': + $page->ajaxReplace[] = array('inner','#gp_option_area',$gp_option_area); + $page->ajaxReplace[] = array('inner','#gp_gallery_avail_imgs',$gp_gallery_avail_imgs); + break; + default: + $content = '
    '.$gp_option_area.'
    ' + .''; + $page->ajaxReplace[] = array('inner','#gp_image_area',$content); + break; + } + + + + $page->ajaxReplace[] = array('inner','#gp_folder_options',$folder_options); + $page->ajaxReplace[] = array('gp_gallery_images','',''); //tell the script the images have been loaded + } + + + /** + * Return folder options for the InlineList + * + */ + public static function InlineList_Options($dir_piece, $folders){ + global $langmessage, $dataDir; + + $return = ''; + $return .= '
    '; + $return .= ' '; + if( strlen($dir_piece) > 23 ){ + $return .= '...'.substr($dir_piece,-20); + }else{ + $return .= $dir_piece; + } + $return .= ''; + + $return .= '
    '; + if( $dir_piece != '/' ){ + $temp = \gp\tool::DirName($dir_piece); + $return .= ' '.$langmessage['create_dir'].''; + $return .= ''; + $return .= ' ../'; + } + + foreach($folders as $folder){ + if( $dir_piece == '/' ){ + $sub_dir = '/'.$folder; + }else{ + $sub_dir = $dir_piece.'/'.$folder; + } + $full_dir = $dataDir.'/data/_uploaded'.$sub_dir; + $sub_files = scandir($full_dir); + $count = 0; + foreach($sub_files as $file){ + if( self::IsImg($file) ){ + $count++; + } + } + $return .= ' '.$count.''.$folder.''; + } + $return .= '
    '; + + return $return; + } + + + /** + * Return folder options for the InlineList + * + */ + public static function InlineList_Folder($image_count, $dir_piece){ + global $langmessage; + + ob_start(); + + if( $image_count > 0 ){ + echo ''.$langmessage['Add All Images'].''; + } + + if( $dir_piece != '/' ){ + + echo '
    '; + self::Max_File_Size(); + echo ''.$langmessage['upload_files'].''; + echo '
    '; + echo ''; + + echo ''; + echo ''; + echo ''; + echo '
    '; + echo '
    '; + } + + return ob_get_clean(); + } + + + /** + * @static + */ + public static function ShowFile_Gallery($dir_piece,$file){ + global $langmessage, $dataDir; + + if( !self::IsImg($file) ){ + return; + } + + //for gallery editing + $rel_path = '/data/_uploaded'.$dir_piece.'/'.$file; + $id = self::ImageId($rel_path); + $file_url = \gp\tool::GetDir($rel_path); + $full_path = $dataDir.$rel_path; + + //thumbnail + $thumb_url = \gp\tool::ThumbnailPath($file_url); + + // alternate text from file name + $img_alt = str_replace('_', ' ', pathinfo($file, PATHINFO_FILENAME) ); + + $thumb = ' '.$img_alt.''; + + //get size + $size = ''; + $size_a = getimagesize($full_path); + if( is_array($size_a) ){ + $size = ' data-width="'.$size_a[0].'" data-height="'.$size_a[1].'"'; + } + + $query_string = 'file_cmd=delete&show=inline&file='.urlencode($file); + + return '
    ' + . '' + . $thumb + . '' + . '' + . \gp\tool::Link( + 'Admin/Uploaded'.$dir_piece, + '', + $query_string, + array( + 'class'=>'delete fa fa-trash gpconfirm', + 'data-cmd'=>'postlink', + 'title'=>$langmessage['delete_confirm'] + ) + ) + . '' + . '
    '; + } + + public static function ImageId($path){ + $encoded = base64_encode($path); + $encoded = rtrim($encoded, '='); + return 'gp_image_'.strtr($encoded, '+/=', '-_.'); + } + + + public function InlineResponse($status,$message){ + echo '
    '; + echo ''; + echo ''; + echo '
    '; + die(); + } + + public function UploadFile($key){ + global $langmessage,$config; + + $fName = $_FILES['userfiles']['name'][$key]; + + $code = (int)$_FILES['userfiles']['error'][$key]; + switch( $code ){ + + case UPLOAD_ERR_OK: + break; + + case UPLOAD_ERR_FORM_SIZE: + case UPLOAD_ERR_INI_SIZE: + $this->errorMessages[] = sprintf($langmessage['upload_error_size'],$this->ReadableMax() ); + return false; + + case UPLOAD_ERR_NO_FILE: + case UPLOAD_ERR_PARTIAL: + $this->errorMessages[] = sprintf($langmessage['UPLOAD_ERROR_PARTIAL'], $fName); + return false; + + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_NO_TMP_DIR: + case UPLOAD_ERR_EXTENSION: + $this->errorMessages[] = sprintf($langmessage['UPLOAD_ERROR'].' ('.$code.')', $fName); + return false; + } + + + $fName = $this->SanitizeName($fName); + $from = $_FILES['userfiles']['tmp_name'][$key]; + + if( !self::AllowedExtension($fName) ){ + return false; + } + + $fName = $this->WindowsName($fName); + $to = $this->FixRepeatNames($fName); + + if( !move_uploaded_file($from,$to) ){ + $this->errorMessages[] = sprintf($langmessage['UPLOAD_ERROR'].' (Move Upload Failed)', $fName); + return false; + } + + @chmod( $to, gp_chmod_file); + + //for images + $file_type = self::GetFileType($fName); + if( isset($this->imgTypes[$file_type]) && function_exists('imagetypes') ){ + + //check the image size if image is not svg/z + if( $config['maximgarea'] > 0 && strpos('svgz', $file_type) !== 0 ){ + \gp\tool\Image::CheckArea($to,$config['maximgarea']); + } + + self::CreateThumbnail($to); + } + + + return $fName; + } + + /** + * Create a thumbnail for the image at the path given by $original + * + */ + public static function CreateThumbnail($original){ + global $config, $dataDir; + + $prefix = $dataDir.'/data/_uploaded'; + $thumb_prefix = $dataDir.'/data/_uploaded/image/thumbnails'; + if( strpos($original,$thumb_prefix) !== false ){ + return; + } + if( strpos($original,$prefix) !== 0 ){ + return; + } + + $thumb_path = \gp\tool::ThumbnailPath($original); + $thumb_dir = \gp\tool::DirName($thumb_path); + + \gp\tool\Files::CheckDir($thumb_dir); + + if( !empty($config['maxthumbheight']) && $config['maxthumbheight'] !== $config['maxthumbsize'] ){ + // NEW TS 5.1: proportional thumbnails + return \gp\tool\Image::CreateRect($original, $thumb_path, $config['maxthumbsize'], $config['maxthumbheight'], $config['thumbskeepaspect']); + }else{ + // return \gp\tool\Image::createSquare($original,$thumb_path,$config['maxthumbsize']); + return \gp\tool\Image::CreateRect($original, $thumb_path, $config['maxthumbsize'], $config['maxthumbsize'], $config['thumbskeepaspect']); + } + } + + + public function FixRepeatNames(&$name){ + + $name_parts = explode('.',$name); + $file_type = array_pop($name_parts); + $temp_name = implode('.',$name_parts); + + $num = 0; + $name = $temp_name.'.'.$file_type; + $to = $this->currentDir.'/'.$name; + while( file_exists($to) ){ + $name = $temp_name.'_'.$num.'.'.$file_type; + $to = $this->currentDir.'/'.$name; + $num++; + } + + return $to; + } + + + /** + * Try to fix file uploads for Windows + * Windows systems don't like long names: MAX_PATH of 260 http://msdn.microsoft.com/en-us/library/aa365247.aspx + */ + public function WindowsName($name){ + + $name_parts = explode('.',$name); + $file_type = array_pop($name_parts); + $temp_name = implode('.',$name_parts); + + $server_software =& $_SERVER['SERVER_SOFTWARE']; + $server_software = strtolower($server_software); + if( strpos($server_software,'win') === false ){ + return $name; + } + + if( isset($this->imgTypes[$file_type]) && function_exists('imagetypes') ){ + $max_len = 260 - strlen($this->currentDir_Thumb); + }else{ + $max_len = 260 - strlen($this->currentDir); + } + + // adjust a minimum of 8 for _#.jpg postfix, / and . characters + $max_len -= (strlen($file_type) + 20); + + if( strlen($temp_name) > $max_len ){ + $temp_name = substr($temp_name,0,$max_len); + } + + return $temp_name.'.'.$file_type; + } + + + + /** + * Check the file extension agains $allowed_types + * + */ + public static function AllowedExtension( &$file , $fix = true ){ + + $file = \gp\tool\Files::NoNull($file); + + if( !gp_restrict_uploads ){ + return true; + } + + + $parts = explode('.',$file); + if( count($parts) < 2 ){ + return true; + } + + + + //make sure the extension is allowed + $file_type = array_pop($parts); + if( !in_array( strtolower($file_type), self::AllowedExtensions() ) ){ + return false; + } + + if( $fix ){ + return implode('_',$parts).'.'.$file_type; + }else{ + return implode('.',$parts).'.'.$file_type; + } + } + + + /** + * Build a list of allowed file extensions + * + * @return array + */ + public static function AllowedExtensions(){ + global $upload_extensions_allow, $upload_extensions_deny, $config; + static $allowed_types; + + if( is_array($allowed_types) ){ + return $allowed_types; + } + + $allowed_types = array(); + + if( is_string($upload_extensions_deny) && strtolower($upload_extensions_deny) === 'all' ){ + $allowed_types = array(); + }else{ + $allowed_types = array( + /** Images **/ 'avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'webp', + /** Media **/ 'aiff', 'asf', 'avi', 'fla', 'flac', 'flv', 'm4v', 'mid', 'mov', 'mp3', 'mp4', 'mpc', 'mpeg', 'mpg', 'ogg', 'oga', 'ogv', 'opus', 'qt', 'ram', 'rm', 'rmi', 'rmvb', 'swf', 'wav', 'wma', 'webm', 'wmv', + /** Archives **/ '7z', 'bz', 'gz', 'gzip', 'rar', 'tar', 'tgz', 'zip', + /** Text/Docs **/ 'css', 'csv', 'doc', 'docx', 'htm', 'html', 'js', 'json', 'less', 'md', 'ods', 'odt', 'pages', 'pdf', 'ppt', 'pptx', 'rtf', 'txt', 'scss', 'sxc', 'sxw', 'vsd', 'webmanifest', 'xls', 'xlsx', 'xml', 'xsl', + /** Fonts **/ 'eot', 'otf', 'ttf', 'woff', 'woff2', + ); + if( !empty($config['allow_svg_upload']) ){ + $allowed_types[] = 'svg'; + } + } + + if( is_array($upload_extensions_allow) ){ + $upload_extensions_allow = array_map('trim',$upload_extensions_allow); + $upload_extensions_allow = array_map('strtolower',$upload_extensions_allow); + $allowed_types = array_merge($allowed_types,$upload_extensions_allow); + } + if( is_array($upload_extensions_deny) ){ + $upload_extensions_allow = array_map('trim',$upload_extensions_allow); + $upload_extensions_allow = array_map('strtolower',$upload_extensions_allow); + $allowed_types = array_diff($allowed_types,$upload_extensions_deny); + } + + $allowed_types = \gp\tool\Plugins::Filter('AllowedTypes',array($allowed_types)); + + return $allowed_types; + } + + + + /** + * Clean a filename by removing unwanted characters + * + */ + public function SanitizeName( $sname ){ + global $config; + + $sname = stripslashes( $sname ) ; + + // Replace dots in the name with underscores (only one dot can be there... security issue). + $sname = preg_replace( '/\\.(?![^.]*$)/', '_', $sname ); + + // Remove \ / | : ? * " < > + return preg_replace( '/\\\\|\\/|\\||\\:|\\?|\\*|"|<|>|[[:cntrl:]]/u', '_', $sname ) ; + } + + + /** + * Delete a single file or folder + * + */ + public function DeleteConfirmed(){ + global $langmessage; + + if( $this->isThumbDir ){ + return false; + } + + if( $_SERVER['REQUEST_METHOD'] != 'POST'){ + msg($langmessage['OOPS'].' (Not POST)'); // using data-cmd="postlink" instead of gpajax + return; + } + + $file = $this->CheckFile(); + if( !$file ){ + return; + } + + $full_path = $this->currentDir.'/'.$file; + $rel_path = '/data/_uploaded'.$this->subdir.'/'.$file; + $thumb_path = \gp\tool::ThumbnailPath($full_path); + + + if( !\gp\tool\Files::RmAll($full_path) ){ + msg($langmessage['OOPS']); + return; + } + + \gp\tool\Files::RmAll($thumb_path); + + + $this->page->ajaxReplace[] = array('img_deleted','',$rel_path); + $this->page->ajaxReplace[] = array('img_deleted_id','',self::ImageId($rel_path)); + } + + /** + * Verify a file is editable or deleteable + * + */ + public function CheckFile($warn = true){ + global $langmessage; + + if( empty($_REQUEST['file']) ){ + if( $warn ) msg($langmessage['OOPS'].'(2)'); + return false; + } + + return $this->CheckFileName($_REQUEST['file'],$warn); + } + + public function CheckFileName($file,$warn){ + global $langmessage; + if( (strpos($file,'/') !== false ) || (strpos($file,'\\') !== false) ){ + if( $warn ) msg($langmessage['OOPS'].'(3)'); + return false; + } + $fullPath = $this->currentDir.'/'.$file; + if( !file_exists($fullPath) ){ + if( $warn ) msg($langmessage['OOPS'].'(4)'); + return false; + } + + if( strpos($fullPath,$this->baseDir) === false ){ + if( $warn ) msg($langmessage['OOPS'].' (5)'); + return false; + } + return $file; + } + + + + + + + /** + * Get the file extension for $file + * @static + * @param string $file The $file name or path + * @return string The extenstion of $file + */ + public static function GetFileType($file){ + $name_parts = explode('.',$file); + $file_type = array_pop($name_parts); + return strtolower($file_type); + } + + /** + * Determines if the $file is an image based on the file extension + * @static + * @return bool + */ + public static function IsImg($file){ + $img_types = array('bmp'=>1,'png'=>1,'jpg'=>1,'jpeg'=>1,'gif'=>1,'tiff'=>1,'tif'=>1,'svg'=>1); + + $type = self::GetFileType($file); + + return isset($img_types[$type]); + } + + + + + /** + * Performs actions after changes are made to files in finder + * + */ + public static function FinderChange($cmd, $result, $args, $finder){ + global $dataDir,$config; + + includeFile('image.php'); + \gp_resized::SetIndex(); + $base_dir = $dataDir.'/data/_uploaded'; + $thumb_dir = $dataDir.'/data/_uploaded/image/thumbnails'; + self::SetRealPath($result,$finder); + + if(isset($result['removed']) or isset($result['added'])) + switch($cmd){ + + case 'rename': + self::RenameResized($result['removed'][0],$result['added'][0]); + break; + + case 'rm': + self::RemoveResized($result['removed']); + break; + + case 'paste': + self::MoveResized($result['removed'],$result['added']); + break; + + //check the image size + case 'upload': + self::MaxSize($result['added']); + break; + } + + + //removed files first + // - Remove associated thumbnail + if( isset($result['removed']) && count($result['removed']) > 0 ){ + foreach($result['removed'] as $removed){ + $removed_path = $removed['realpath']; + \gp\tool\Plugins::Action('FileDeleted',$removed_path); + + $thumb_path = str_replace($base_dir,$thumb_dir,$removed_path); + if( file_exists($thumb_path) ){ + if( is_dir($thumb_path) ){ + \gp\tool\Files::RmAll($thumb_path); + }else{ + unlink($thumb_path); + } + continue; + } + + // svg or not svg + $nameParts = explode('.',$removed_path); + $type = array_pop($nameParts); + $type = strtolower($type); + if( strpos('svgz',$type) !== 0 ){ + $type = 'jpg'; + } + + $thumb_path = str_replace($base_dir,$thumb_dir,$removed_path).'.'.$type; + if( file_exists($thumb_path) ){ + unlink($thumb_path); + } + } + } + + + //added files + self::FinderActions($result, 'added', 'FileUploaded'); + + //changed files (resized) + self::FinderActions($result, 'changed', 'FileChanged'); + + \gp_resized::SaveIndex(); + + //debug + /* + $log_file = $dataDir.'/data/_temp/finder_log-'.time().'.txt'; + $data = get_defined_vars(); + $content = print_r($data,true).'
    '; + $fp = fopen($log_file,'a'); + fwrite($fp,$content); + */ + } + + /** + * Call Actions on the finder result + * + */ + protected static function FinderActions($result, $key, $action){ + + if( isset($result[$key]) && count($result[$key]) > 0 ){ + foreach($result[$key] as $changed){ + \gp\tool\Plugins::Action($action,$changed['realpath']); + self::CreateThumbnail($changed['realpath']); + } + } + } + + /** + * Make sure newly uploaded images are within the site's max-size setting + * + */ + public static function MaxSize($added){ + global $config; + + if( $config['maximgarea'] > 0 ){ + foreach($added as $file){ + \gp\tool\Image::CheckArea($file['realpath'],$config['maximgarea']); + } + } + } + + /** + * Move + * + */ + public static function MoveResized($removed,$added){ + global $dataDir; + + + //separate removed and moved entries + $moved = array(); + $new_removed = array(); + foreach($added as $akey => $ainfo){ + $source = isset($ainfo['source']) ? $ainfo['source'] : null; + foreach($removed as $rkey => $rinfo){ + if( $source == $rinfo['realpath'] ){ + $moved[$akey] = $rinfo; + }else{ + $new_removed[$rkey] = $rinfo; + } + } + } + + //remove files that weren't moved + self::RemoveResized($new_removed); + + + //rename files that were moved + foreach($added as $akey => $ainfo){ + $rinfo = isset($moved[$akey]) ? $moved[$akey] : null; + self::RenameResized($rinfo,$ainfo); + } + } + + /** + * Remove all of the resized images for an image that is deleted + * + */ + public static function RemoveResized($removed){ + global $dataDir; + + foreach($removed as $key => $info){ + $img = self::TrimBaseDir($info['realpath']); + $index = array_search($img,\gp_resized::$index); + if( !$index ){ + continue; + } + unset(\gp_resized::$index[$index]); + $folder = $dataDir.'/data/_resized/'.$index; + if( file_exists($folder) ){ + \gp\tool\Files::RmAll($folder); + } + } + } + + + + /** + * Update the name of an image in the index when renamed + * + */ + public static function RenameResized($removed,$added){ + $added_img = self::TrimBaseDir($added['realpath']); + $removed_img = self::TrimBaseDir($removed['realpath']); + $index = array_search($removed_img,\gp_resized::$index); + if( !$index ){ + return false; + } + \gp_resized::$index[$index] = $added_img; + } + + + /** + * Make sure the realpath value is set for finder arrays + * + */ + public static function SetRealPath(&$array,$finder){ + foreach($array as $type => $list){ + if( !is_array($list) ){ + continue; + } + foreach($list as $key => $info){ + if( isset($info['hash']) && !isset($info['realpath']) ){ + $array[$type][$key]['realpath'] = $finder->realpath($info['hash']); + } + if( isset($array[$type][$key]['realpath']) ){ + $array[$type][$key]['realpath'] = \gp\tool::WinPath($array[$type][$key]['realpath']); + } + } + } + } + + + /** + * Get a relative file path by stripping the base dir off of a full path + * + */ + public static function TrimBaseDir($full_path){ + global $dataDir; + + $base_dir = $dataDir.'/data/_uploaded'; + $len = strlen($base_dir); + if( strpos($full_path,$base_dir) === 0 ){ + return substr($full_path,$len); + } + return $full_path; + } + + } +} + +namespace{ + class admin_uploaded extends \gp\admin\Content\Uploaded{} +} diff --git a/include/admin/Layout.php b/include/admin/Layout.php new file mode 100644 index 0000000..fb36eb9 --- /dev/null +++ b/include/admin/Layout.php @@ -0,0 +1,2291 @@ + -ident-, 1 => -ident-] + $gpLayout['Loyout_Name']['color'] = '#123456' + $gpLayout['Loyout_Name']['theme'] = 'One_Point_5/Blue' + +*/ + + +class Layout extends \gp\admin\Addon\Install{ + + public $curr_layout; + protected $layout_request = false; + protected $LayoutArray; + protected $scriptUrl = 'Admin_Theme_Content'; + protected $versions = []; + + + //remote install variables + public $config_index = 'themes'; + public $code_folder_name = '_themes'; + public $path_remote = 'Admin_Theme_Content/Remote'; + + private $gpLayouts_before; + private $config_before; + + + public function __construct($args){ + global $gpLayouts, $config; + + parent::__construct($args); + + $this->gpLayouts_before = $gpLayouts; + $this->config_before = $config; + + if( $this->page ){ + $this->page->head_js[] = '/include/js/theme_content.js'; + $this->page->head_js[] = '/include/js/dragdrop.js'; + $this->page->css_admin[] = '/include/css/theme_content.scss'; + } + + \gp\tool::LoadComponents('resizable'); + + $this->GetPossible(); + } + + + public function RunScript(){ + global $config, $gpLayouts, $langmessage; + + $cmd = \gp\tool::GetCommand(); + + //set current layout + $this->curr_layout = $config['gpLayout']; + if( isset($_REQUEST['layout']) ){ + $this->curr_layout = $_REQUEST['layout']; + } + if( !array_key_exists($this->curr_layout,$gpLayouts) ){ + msg($langmessage['OOPS'].' (Invalid Layout)'); + $cmd = ''; + } + + $this->SetLayoutArray(); + + //Installation + $this->cmds['remote_install'] = 'RemoteInstall'; + $this->cmds['RemoteInstall'] = ''; + $this->cmds['RemoteInstallConfirmed'] = 'DefaultDisplay'; + $this->cmds['UpgradeTheme'] = 'DefaultDisplay'; + + //Copy, Delete + $this->cmds['CopyLayoutPrompt'] = ''; + $this->cmds['CopyLayout'] = 'DefaultDisplay'; + $this->cmds['DeleteLayout'] = 'DefaultDisplay'; + + //Reviews + $this->cmds['SendAddonReview'] = ''; + $this->cmds['ReviewAddonForm'] = ''; + + $this->cmds['addontext'] = 'RedirectText'; + + $this->LayoutCommands(); + $this->RunCommands($cmd); + } + + + /** + * Redirect addontext requests to correct path for TS 5.0+ + * + */ + protected function RedirectText(){ + $params = $_GET; + $params['cmd'] = 'AddonTextForm'; + + $url = \gp\tool::GetUrl( + 'Admin_Theme_Content/Text', + http_build_query($params, '', '&'), + false + ); + \gp\tool::Redirect($url); + } + + + /** + * Perform various layout commands + * + */ + public function LayoutCommands(){ + + $this->cmds['ShowTitles'] = ''; + $this->cmds['ShowGadgets'] = ''; + $this->cmds['LayoutLabel'] = ''; + $this->cmds['MakeDefault'] = 'DefaultDisplay'; + $this->cmds['CSSPreferences'] = ''; + $this->cmds['RestoreLayout'] = 'DefaultDisplay'; + $this->cmds['RmGadget'] = 'ShowGadgets'; + } + + + /** + * Show all layouts and themes + * + */ + public function DefaultDisplay(){ + global $config, $langmessage, $gpLayouts; + + $this->page->head_js[] = '/include/js/auto_width.js'; + + $this->ShowHeader(); + + echo '
    '; + + //all other layouts + foreach($gpLayouts as $layout => $info){ + $this->LayoutDiv($layout,$info); + } + echo '
    '; + + echo '
    '; + echo '

    '; + echo $langmessage['see_also'] . ' '; + echo \gp\tool::Link('Admin/Menu', $langmessage['file_manager']); + echo '

    '; + + $this->ColorSelector(); + } + + + /** + * Display a list of all the titles using the current layout + * + */ + public function ShowTitles(){ + global $langmessage; + + //affected titles + $titles_count = $this->TitlesCount($this->curr_layout); + + echo '

    ' . $langmessage['titles_using_layout']; + echo ': ' . $titles_count; + echo '

    '; + + if( $titles_count > 0 ){ + echo '
      '; + + foreach( $this->LayoutArray as $index => $layout_comparison ){ + if( $this->curr_layout == $layout_comparison ){ + + $title = \gp\tool::IndexToTitle($index); + if( empty($title) ){ + continue; //may be external link + } + + echo "\n" . '
    • '; + $label = \gp\tool::GetLabel($title); + $label = \gp\tool::LabelSpecialChars($label); + echo \gp\tool::Link($title, $label); + echo '
    • '; + } + } + + echo '
    '; + echo '
    '; + } + } + + + /** + * Display gadgets and their status for the current layout + * + */ + public function ShowGadgets(){ + global $langmessage, $config; + + $gadget_info = \gp\tool\Output::WhichGadgets($this->curr_layout); + + echo '

    ' . $langmessage['gadgets'] . '

    '; + echo ''; + echo ''; + + if( !isset($config['gadgets']) || count($config['gadgets']) == 0 ){ + echo ''; + }else{ + foreach($config['gadgets'] as $gadget => $temp){ + echo ''; + } + } + echo '
     
    '; + echo $langmessage['Empty']; + echo '
    '; + echo str_replace('_', ' ', $gadget); + echo ''; + if( isset($gadget_info[$gadget]) ){ + echo $this->LayoutLink( + $this->curr_layout, + $langmessage['remove'], + 'cmd=RmGadget&gadget=' . urlencode($gadget), + ['data-cmd' => 'gpabox'] + ); + }else{ + echo $langmessage['disabled']; + } + echo '
    '; + } + + + /** + * Create a drop-down menu for the layout options + * + */ + public function LayoutOptions($layout,$info){ + global $langmessage, $config; + + //theme name + echo '
  • '; + echo '' . $langmessage['theme'] . ': '; + echo $this->ThemeLabel($info['theme_name']); + echo ''; + echo '
  • '; + + //default + echo '
  • '; + if( $config['gpLayout'] == $layout ){ + echo '' . $langmessage['default'] . ''; + }else{ + echo \gp\tool::Link( + 'Admin_Theme_Content', + $langmessage['make_default'], + 'cmd=MakeDefault&layout=' . rawurlencode($layout), + [ + 'data-cmd' => 'creq', + 'title' => $langmessage['make_default'], + ] + ); + } + echo '
  • '; + + //gadgets + echo '
  • '; + echo $this->LayoutLink( + $layout, + $langmessage['gadgets'], + 'cmd=ShowGadgets', + ['data-cmd' => 'gpabox'] + ); + echo '
  • '; + + + //titles using layout + echo '
  • '; + $titles_count = $this->TitlesCount($layout); + $label = sprintf($langmessage['%s Pages'], $titles_count); + if( $titles_count ){ + echo $this->LayoutLink( + $layout, + $label, + 'cmd=ShowTitles', + ['data-cmd' => 'gpabox'] + ); + }else{ + echo '' . $label . ''; + } + echo '
  • '; + + + //content arrangement + $handlers_count = $this->HandlersCount($info); + echo '
  • '; + if( $handlers_count ){ + echo $this->LayoutLink( + $layout, + $langmessage['restore_defaults'], + 'cmd=RestoreLayout', + ['data-cmd' => 'creq'] + ); + }else{ + echo ''; + echo $langmessage['content_arrangement'] . ': ' . $langmessage['default']; + echo ''; + } + echo '
  • '; + + //copy + echo '
  • '; + $query = 'cmd=CopyLayoutPrompt&layout=' . $layout; + echo \gp\tool::Link( + 'Admin_Theme_Content', + $langmessage['Copy'], + $query, + ['data-cmd' => 'gpabox'] + ); + echo '
  • '; + + //delete + if( $config['gpLayout'] != $layout ){ + echo '
  • '; + $attr = [ + 'data-cmd' => 'creq', + 'class' => 'gpconfirm', + 'title' => sprintf($langmessage['generic_delete_confirm'], $info['label']), + ]; + echo \gp\tool::Link( + 'Admin_Theme_Content', + $langmessage['delete'], + 'cmd=deletelayout&layout=' . $layout, + $attr + ); + echo '
  • '; + } + } + + + /** + * Get number of handlers + * + */ + public function HandlersCount($layout_info){ + + $handlers_count = 0; + if( isset($layout_info['handlers']) && is_array($layout_info['handlers']) ){ + foreach($layout_info['handlers'] as $val){ + $int = count($val); + if( $int === 0){ + $handlers_count++; + } + $handlers_count += $int; + } + } + + return $handlers_count; + } + + + /** + * Get the layout customizer array if customizer.php exists + * @since 5.2 + * @param string the layout id + * @return array layout customizer + */ + public function GetLayoutCustomizer($layout){ + + $layout_info = \gp\tool::LayoutInfo($layout, false); + $dir = $layout_info['dir'] . '/' . $layout_info['theme_color']; + $customizer_file = $dir . '/customizer.php'; + + if( file_exists($customizer_file) ){ + return \gp\tool\Files::Get($customizer_file, 'customizer'); + } + + return []; + } + + + /** + * Get the custom config for a layout if it exists + * @since 5.2 + * @param string the layout id + * @return array layout configuration + * + */ + public function GetLayoutConfig($layout){ + + $config_file = \gp\tool\Output::LayoutConfigFile($layout); + + if( file_exists($config_file) ){ + $config = \gp\tool\Files::Get($config_file, 'config'); + if( !empty($config) ){ + return $config; + } + } + + // not yet saved? - get the defaults from customizer + return $this->GetLayoutDefaultConfig($layout); + } + + + /** + * Extract default config values from customizer definition file + * @since 5.2 + * @param string the layout id + * @return array default config + * + */ + public function GetLayoutDefaultConfig($layout){ + + $customizer_data = $this->GetLayoutCustomizer($layout); + if( empty($customizer_data) ){ + return []; + } + + $default_config = []; + + foreach( $customizer_data as $area => $area_info ){ + foreach( $area_info['items'] as $var_name => $var_data ){ + $default_config[$var_name] = []; + $default_config[$var_name]['value'] = $var_data['default_value']; + if( !empty($var_data['default_units']) ){ + $default_config[$var_name]['units'] = $var_data['default_units']; + } + } + } + + return $default_config; + } + + + /** + * Get the path of the customizer css file + * @since 5.2 + * @param string the layout id + * @return string + * + */ + public function GetCustomizerCSSFile($layout){ + + $layout_info = \gp\tool::LayoutInfo($layout, false); + $dir = $layout_info['dir'] . '/' . $layout_info['theme_color']; + $style_type = \gp\tool\Output::StyleType($dir); + + return \gp\tool\Output::CustomizerStyleFile($layout, $style_type); + } + + + /** + * @deprecated 5.2 + * use GetLayoutCSS instead + */ + public function LayoutCSS($layout){ + return $this->GetLayoutCSS($layout); + } + + /** + * Get the custom css for a layout if it exists + * @since 5.2 + * @param string the layout id + * @return string CSS, SCSS or LESS + */ + public function GetLayoutCSS($layout){ + + $custom_css_file = $this->GetLayoutCSSFile($layout); + + if( file_exists($custom_css_file) ){ + return file_get_contents($custom_css_file); + } + + return ''; + } + + + /** + * @deprecated 5.2 + * use SaveCustomCSS instead + */ + public function SaveCustom($layout, $css){ + $this->SaveCustomCSS($layout, $css); + } + + /** + * Save the custom.css / .less / .scss file + * @since 5.2 + * @param string the layout id + * @param string CSS, SCSS or LESS + * @return bool success + * + */ + public function SaveCustomCSS($layout, $css){ + global $langmessage; + + $custom_file = $this->GetLayoutCSSFile($layout); + + //delete css file if empty + if( empty($css) ){ + return $this->RemoveCSS($layout, $custom_file); + } + + //save if not empty + if( !\gp\tool\Files::Save($custom_file, $css) ){ + msg($langmessage['OOPS'] . ' (CSS not saved)'); + return false; + } + + return true; + } + + + /** + * Save the data posted by customizer + * @since 5.2 + * @param string the layout id + * @param array customizer results + * @return bool success + * + */ + public function SaveCustomizerResults($layout, $customizer_results){ + + $success = true; + + // customizer css + $customizer_css = ''; + if( !empty($customizer_results['scssless_vars']) ){ + $customizer_css .= $customizer_results['scssless_vars']; + } + if( !empty($customizer_results['css_vars']) ){ + $customizer_css .= $customizer_results['css_vars']; + } + $success = $success && $this->SaveCustomizerCSS($layout, $customizer_css); + + // layout config + $layout_config = !empty($customizer_results['layout_config']) ? + $customizer_results['layout_config'] : + []; + + $success = $success && $this->SaveLayoutConfig($layout, $layout_config); + + return $success; + } + + + /** + * Save the layout config from customizer + * @since 5.2 + * @param string the layout id + * @param array layout config + * @return bool success + * + */ + public function SaveLayoutConfig($layout, $layout_config){ + global $langmessage; + + $success = true; + + $layout_config_file = \gp\tool\Output::LayoutConfigFile($layout); + + if( empty($layout_config) ){ + // delete layout config file if empty + $success = $success && $this->RemoveLayoutConfig($layout); + // save it otherwise + }elseif( !\gp\tool\Files::SaveData($layout_config_file, 'config', $layout_config) ){ + msg($langmessage['OOPS'] . ' (Layout config not saved)'); + $success = false; + } + + return $success; + } + + /** + * Get the customizer css for a layout if it exists + * @since 5.2 + * @param string the layout id + * @return string CSS, SCSS or LESS + */ + public function GetCustomizerCSS($layout){ + + $customizer_css_file = $this->GetCustomizerCSSFile($layout); + + if( file_exists($customizer_css_file) ){ + return file_get_contents($customizer_css_file); + } + + return ''; + } + + /** + * Save the customizer css + * @since 5.2 + * @param string the layout id + * @param array customizer results + * @return bool success + * + */ + public function SaveCustomizerCSS($layout, $customizer_css){ + global $langmessage; + + $success = true; + + $customizer_css_file = $this->GetCustomizerCSSFile($layout); + + if( empty($customizer_css) ){ + // delete css file if empty + $success = $success && $this->RemoveCustomizerCSS($layout, $customizer_css_file); + // save it otherwise + }elseif( !\gp\tool\Files::Save($customizer_css_file, $customizer_css) ){ + msg($langmessage['OOPS'] . ' (Customizer stylesheet not saved)'); + $success = false; + } + + return $success; + } + + + /** + * @deprecated 5.2 + * use GetLayoutCSSFile instead + */ + public function LayoutCSSFile($layout){ + return $this->GetLayoutCSSFile(); + } + + + /** + * Get the path of the custom css file + * @since 5.2 + * @param string the layout id + * @return string + * + */ + public function GetLayoutCSSFile($layout){ + + $layout_info = \gp\tool::LayoutInfo($layout, false); + $dir = $layout_info['dir'] . '/' . $layout_info['theme_color']; + $style_type = \gp\tool\Output::StyleType($dir); + + return \gp\tool\Output::CustomStyleFile($layout, $style_type); + } + + + /** + * Remove the custom css file and unset the 'css' key from a layout + * @param string the layout id + * @param string custom file abspath + * @return bool success + */ + public function RemoveCSS($layout, $custom_file){ + global $gpLayouts; + + if( file_exists($custom_file) ){ + unlink($custom_file); + } + + if( isset($gpLayouts[$layout]['css']) ){ + unset($gpLayouts[$layout]['css']); + } + + return true; + } + + + /** + * Remove the customizer.scss/less/css file for a layout + * @since 5.2 + * @param string the layout id + * @return bool success + * + */ + public function RemoveCustomizerCSS($layout){ + global $gpLayouts; + + $customizer_css_file = $this->GetCustomizerCSSFile($layout); + + if( file_exists($customizer_css_file) ){ + unlink($customizer_css_file); + } + + if( isset($gpLayouts[$layout]['customizer_css']) ){ + unset($gpLayouts[$layout]['customizer_css']); + } + + return true; + } + + + /** + * Remove the custom config.php file for a layout + * @since 5.2 + * @param string the layout id + * @return bool success + * + */ + public function RemoveLayoutConfig($layout){ + global $gpLayouts; + + $config_file = \gp\tool\Output::LayoutConfigFile($layout); + + if( file_exists($config_file) ){ + unlink($config_file); + } + + if( isset($gpLayouts[$layout]['config']) ){ + unset($gpLayouts[$layout]['config']); + } + + return true; + } + + + /** + * Entirely remove the custom layout data direcory and its content + * @since 5.2 + * @param string the layout directory + * @return bool success + * + */ + public function RemoveCustomDir($layout){ + global $langmessage, $dataDir; + + $dir = $dataDir . '/data/_layouts/' . $layout; + + if( !file_exists($dir) ){ + return false; + } + + $remove_files = [ + $dir . '/custom.css', + $dir . '/custom.less', + $dir . '/custom.scss', + $dir . '/customizer.css', + $dir . '/customizer.less', + $dir . '/customizer.scss', + $dir . '/config.php', + $dir . '/index.html', + ]; + + foreach($remove_files as $path){ + if( file_exists($path) ){ + @unlink($path); + } + } + + if( !\gp\tool\Files::RmDir($dir) ){ + msg( + $langmessage['OOPS'] . + ' Cannot remove /data/_layouts/' . + htmlspecialchars($layout) . + '. Directory is not empty.' + ); + return false; + }; + + return true; + } + + + /** + * Save changes to the css settings for a layout + * + */ + public function CSSPreferences(){ + global $langmessage, $gpLayouts; + + $new_info = $gpLayouts[$this->curr_layout]; + + if( isset($_POST['menu_css_ordered']) ){ + if( $_POST['menu_css_ordered'] === 'off' ){ + $new_info['menu_css_ordered'] = false; + }else{ + unset($new_info['menu_css_ordered']); + } + } + + if( isset($_POST['menu_css_indexed']) ){ + if( $_POST['menu_css_indexed'] === 'off' ){ + $new_info['menu_css_indexed'] = false; + }else{ + unset($new_info['menu_css_indexed']); + } + } + + $gpLayouts[$this->curr_layout] = $new_info; + + if( !$this->SaveLayouts(false) ){ + return; + } + + if( $this->layout_request || $this->page->gpLayout == $this->curr_layout ){ + $this->page->SetTheme($this->curr_layout); + } + + + $content = $this->CSSPreferenceForm($this->curr_layout,$new_info); + $this->page->ajaxReplace = []; + $this->page->ajaxReplace[] = [ + 'replace', + '#layout_css_ul_' . $this->curr_layout, + $content + ]; + } + + + /** + * Remove a gadget from a layout + * @return null + * + */ + public function RmGadget(){ + global $langmessage; + + //$this->page->ajaxReplace = []; + + $gadget =& $_REQUEST['gadget']; + + $handlers = $this->GetAllHandlers($this->curr_layout); + + //make sure GetAllGadgets is set + $this->PrepContainerHandlers( + $handlers, + 'GetAllGadgets', + 'GetAllGadgets' + ); + + $changed = false; + foreach($handlers as $container => $container_info){ + foreach($container_info as $key => $gpOutCmd){ + if( $gpOutCmd == $gadget ){ + $changed = true; + unset($handlers[$container][$key]); + } + } + } + + if( !$changed ){ + msg($langmessage['OOPS'] . ' (Not Changed)'); + return; + } + + $this->SaveHandlersNew($handlers, $this->curr_layout); + } + + + public static function GetRandColor(){ + $colors = self::GetColors(); + $color_key = array_rand($colors); + return $colors[$color_key]; + } + + + public static function GetColors(){ + return [ + '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#9900ff', '#ff00ff', + '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#cfe2f3', '#d9d2e9', '#ead1dc', + '#ea9999', '#f9cb9c', '#ffe599', '#b6d7a8', '#a2c4c9', '#9fc5e8', '#b4a7d6', '#d5a6bd', + '#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6fa8dc', '#8e7cc3', '#c27ba0', + '#cc0000', '#e69138', '#f1c232', '#6aa84f', '#45818e', '#3d85c6', '#674ea7', '#a64d79', + '#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#0b5394', '#351c75', '#741b47', + ]; + } + + + /** + * Update theme hooks and references in any related layouts + * + * + */ + public function UpgradeTheme(){ + global $langmessage, $gpLayouts; + + $theme =& $_REQUEST['source']; + $theme_info = $this->ThemeInfo($theme); + + if( !$theme_info ){ + msg($langmessage['OOPS'] . ' (Invalid Source)'); + return false; + } + + //install addon + $installer = new \gp\admin\Addon\Installer(); + $installer->addon_folder_rel = dirname($theme_info['rel']); + $installer->code_folder_name = '_themes'; + $installer->source = $theme_info['full_dir']; + $success = $installer->Install(); + + $installer->OutputMessages(); + + if( !$success ){ + return; + } + + $this->UpdateLayouts($installer); + } + + + /** + * Update related layouts with new $theme_info + * + */ + public function UpdateLayouts($installer){ + global $gpLayouts, $langmessage; + + $theme_folder = basename($installer->dest); + + if( strpos($installer->dest, '/data/_themes') !== false ){ + $new_layout_info = $this->AvailableTheme('/data/_themes', true, $theme_folder); + }else{ + $new_layout_info = $this->AvailableTheme('/themes', false, $theme_folder); + } + + if( $new_layout_info === false ){ + return; + } + + if( $installer->has_hooks ){ + $new_layout_info['addon_key'] = $installer->config_key; + } + + // update each layout + foreach($gpLayouts as $layout => $layout_info){ + + if( !$this->SameTheme($layout_info, $new_layout_info) ){ + continue; + } + + unset( + $layout_info['is_addon'], + $layout_info['addon_id'], + $layout_info['version'], + $layout_info['name'], + $layout_info['addon_key'] + ); + + $layout_info += $new_layout_info; + $layout_info['theme'] = $theme_folder . '/' . basename($layout_info['theme']); + $gpLayouts[$layout] = $layout_info; + } + + $this->SaveLayouts(); + } + + + /** + * Return true if two layouts use the same theme + * + */ + public function SameTheme($layout_info, $new_layout_info ){ + + //if we have addon ids + if( isset($new_layout_info['addon_id']) && + isset($layout_info['addon_id']) && + $layout_info['addon_id'] == $new_layout_info['addon_id'] + ){ + return true; + } + + if( isset($layout_info['is_addon']) && $layout_info['is_addon'] ){ + $layout_info['rel'] = '/data/_themes/' . dirname($layout_info['theme']); + }else{ + $layout_info['rel'] = '/themes/' . dirname($layout_info['theme']); + } + + $keys = ['is_addon' => '', 'rel' => '']; + $testa = array_intersect_key($layout_info, $keys); + $testb = array_intersect_key($new_layout_info, $keys); + if( $testa === $testb ){ + return true; + } + + return false; + } + + + /** + * + */ + public function RemoteInstallConfirmed($type='themes'){ + $installer = parent::RemoteInstallConfirmed($type); + $this->GetPossible(); + $this->UpdateLayouts($installer); + } + + + /** + * Display some options before copying a layout + * + */ + public function CopyLayoutPrompt(){ + global $langmessage, $gpLayouts; + + $layout = $this->ReqLayout(); + if( $layout === false ){ + return; + } + + $label = self::NewLabel($gpLayouts[$layout]['label']); + + echo '

    ' . $langmessage['new_layout'] . '

    '; + echo '
    '; + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo '
    '; + echo $langmessage['options']; + echo '
    '; + echo $langmessage['label']; + echo ''; + echo ''; + echo '
    '; + echo $langmessage['make_default']; + echo ''; + echo ''; + echo '
    '; + + echo '

    '; + echo ' '; + echo ' '; + echo ' '; + echo '

    '; + echo '
    '; + + } + + /** + * Copy a layout + * + */ + public function CopyLayout(){ + global $gpLayouts, $langmessage; + + $copy_id = $this->ReqLayout(); + if( $copy_id === false ){ + return; + } + + if( empty($_POST['label']) ){ + msg($langmessage['OOPS'] . '(Empty Label)'); + return; + } + + $newLayout = $gpLayouts[$copy_id]; + $newLayout['color'] = self::GetRandColor(); + $newLayout['label'] = htmlspecialchars($_POST['label']); + + //get new unique layout id + do{ + $layout_id = rand(1000, 9999); + }while(isset($gpLayouts[$layout_id])); + + $gpLayouts[$layout_id] = $newLayout; + + if( !\gp\tool\Files::ArrayInsert($copy_id, $layout_id, $newLayout, $gpLayouts, 1) ){ + msg($langmessage['OOPS'] . '(Not Inserted)'); + return; + } + + $success = true; + + //copy any css + $css = $this->GetLayoutCSS($copy_id); + if( !$this->SaveCustom($layout_id, $css) ){ + $success = false; + } + + //copy possible customizer css + $customizer_css = $this->GetCustomizerCSS($copy_id); + if( !empty($customizer_css) && !$this->SaveCustomizerCSS($layout_id, $customizer_css) ){ + $success = false; + } + + //copy possible layout config + $layout_config = $this->GetLayoutConfig($copy_id); + if( !empty($layout_config) && !$this->SaveLayoutConfig($layout_id, $layout_config) ){ + $success = false; + } + + $this->SaveLayouts(); + } + + + /** + * Save the gpLayouts data + * + */ + protected function SaveLayouts($notify_user=true){ + global $gpLayouts; + + if( \gp\admin\Tools::SavePagesPHP($notify_user, $notify_user) ){ + return true; + } + + if( is_array($this->gpLayouts_before) ){ + $gpLayouts = $this->gpLayouts_before; + } + return false; + } + + + /** + * Save the config setting + * NOTE: This is not layout config but global system config! + */ + protected function SaveConfig(){ + global $config; + + if( \gp\admin\Tools::SaveConfig(true, true) ){ + return true; + } + + if( is_array($this->config_before) ){ + $config = $this->config_before; + } + return false; + } + + + /** + * Create a new unique layout label + * @static + */ + public function NewLabel($label){ + global $gpLayouts; + $labels = []; + + foreach($gpLayouts as $info){ + $labels[$info['label']] = true; + } + + $len = strlen($label); + if( $len > 25 ){ + $label = substr($label,0,$len-2); + } + if( substr($label, $len - 2, 1) === '_' && is_numeric(substr($label, $len - 1, 1)) ){ + $label = substr($label, 0, $len - 2); + } + + $int = 1; + do{ + $new_label = $label . '_' . $int; + $int++; + }while(isset($labels[$new_label])); + + return $new_label; + } + + + public function LoremIpsum(){ + global $langmessage, $gp_titles, $gp_menu; + + ob_start(); + + echo '

    H1 Lorem Ipsum Heading

    '; + + echo '

    Paragraph (larger): Lorem ipsum dolor sit amet, consectetur adipisicing elit, '; + echo 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '; + echo 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    '; + + echo '

    H2 Lorem Ipsum Heading

    '; + + echo '

    Paragraph: Excepteur sint emphasize cupidatat non strong proident, sunt in '; + echo 'emphasized strong culpa qui officia anchor '; + echo 'deserunt underline mollit anim id est laborum. Duis aute irure dolor in reprehenderit in voluptate '; + echo 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui '; + echo 'abbr officia deserunt mollit mark anim id est code laborum.

    '; + + echo '
    Blockquote: Lorem ipsum dolor sit amet, consectetur adipisicing elit.
    '; + + echo '
    '; + + echo '
    '; + echo '

    Unordered list

    '; + echo '
      '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    '; + echo '
    '; // /.gpCol-4 + + echo '
    '; + echo '

    Ordered list

    '; + echo '
      '; + echo '
    1. Lorem Ipsum ordered list item
    2. '; + echo '
    3. Lorem Ipsum ordered list item
    4. '; + echo '
    5. Lorem Ipsum ordered list item
    6. '; + echo '
    7. Lorem Ipsum ordered list item
    8. '; + echo '
    '; + echo '
    '; // /.gpCol-4 + + echo '
    '; + echo '

    Description list

    '; + echo '
    '; + echo '
    Lorem Ipsum term
    Lorem Ipsum description
    '; + echo '
    Lorem Ipsum term
    Lorem Ipsum description
    '; + echo '
    '; + echo '
    '; // /.gpCol-4 + + echo '
    '; // /.gpRow + + echo '
    '; + + echo '
    '; + + echo '
    '; + echo '

    H3 Lorem Ipsum Heading

    '; + echo '

    H4 Lorem Ipsum Heading

    '; + echo '
    H5 Lorem Ipsum Heading
    '; + echo '
    H6 Lorem Ipsum Heading
    '; + echo '

    Paragraph (smaller): Excepteur sint cupidatat non proident, sunt in '; + echo 'culpa qui officia deserunt mollit anim id est laborum. Duis aute irure dolor in reprehenderit '; + echo 'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat '; + echo 'non proident.

    '; + echo '
    '; // /.gpCol-6 + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
    Unstyled Table - Heading
    Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
    Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
    Row 3, Cell 1Row 3, Cell 2Row 3, Cell 3
    Row 4, Cell 1Row 4, Cell 2Row 4, Cell 3
    Row 5, Cell 1Row 5, Cell 2Row 5, Cell 3
    '; + echo '
    '; // /.gpCol-6 + + echo '
    '; // /.gpRow + + echo '
    '; + + $this->page->non_admin_content = ob_get_clean(); + + // boostrap content + ob_start(); + + echo '

    H1 Lorem Ipsum Heading + small

    '; + + echo '

    Lead: Lorem ipsum dolor sit amet, consectetur adipisicing elit, '; + echo 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '; + echo 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    '; + + echo '

    H2 Lorem Ipsum Heading + small

    '; + + echo '

    Default paragraph: Excepteur sint emphasize cupidatat non strong proident, sunt in '; + echo 'emphasized strong culpa qui officia anchor '; + echo 'deserunt underline mollit anim id est laborum. Duis aute irure dolor in reprehenderit in voluptate '; + echo 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint kbd occaecat cupidatat non proident, sunt in culpa qui '; + echo 'abbr officia deserunt mollit mark anim id est code laborum.

    '; + + echo '

    '; + echo 'text-muted   '; + echo 'text-primary   '; + echo 'text-secondary   '; + echo 'text-success   '; + echo 'text-info   '; + echo 'text-warning   '; + echo 'text-danger   '; + echo 'badge'; + // echo '    label label-default'; + echo '

    '; + + echo '
    Blockquote: Lorem ipsum dolor sit amet, consectetur adipisicing elit.
    '; + + echo '
    '; + + echo '
    '; + echo '

    Unordered list

    '; + echo '
      '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    • Lorem Ipsum unordered list item
    • '; + echo '
    '; + echo '
    '; // /.col-md-4 + + echo '
    '; + echo '

    Ordered list

    '; + echo '
      '; + echo '
    1. Lorem Ipsum ordered list item
    2. '; + echo '
    3. Lorem Ipsum ordered list item
    4. '; + echo '
    5. Lorem Ipsum ordered list item
    6. '; + echo '
    7. Lorem Ipsum ordered list item
    8. '; + echo '
    '; + echo '
    '; // /.col-md-4 + + echo '
    '; + echo '

    Description list

    '; + echo '
    '; + echo '
    Lorem Ipsum term
    Lorem Ipsum description
    '; + echo '
    Lorem Ipsum term
    Lorem Ipsum description
    '; + echo '
    '; + echo '
    '; // /.col-md-4 + + echo '
    '; // /.row + + echo '
    '; + + echo '
    '; + echo '

    H3 Lorem Ipsum Heading + small

    '; + echo '

    H4 Lorem Ipsum Heading + small

    '; + echo '
    H5 Lorem Ipsum Heading + small
    '; + echo '
    H6 Lorem Ipsum Heading + small
    '; + echo '

    Small text paragraph: Excepteur sint cupidatat non proident, sunt in '; + echo 'culpa qui officia deserunt mollit anim id est laborum. Duis aute irure dolor in reprehenderit '; + echo 'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat '; + echo 'non proident. '; + echo '

    '; + echo '
    '; + echo '

    '; + echo 'btn-primary   '; + echo 'btn-secondary   '; + echo 'btn-success'; + echo '

    '; + echo '
    '; + echo '

    '; + echo 'btn-info   '; + echo 'btn-warning   '; + echo 'btn-danger'; + echo '

    '; + echo '
    '; + echo '

    '; + echo 'btn-default   '; + echo 'btn-link'; + echo '

    '; + echo '
    '; + echo '
    '; // /.col-md-6 + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
    Table Heading'; + echo ''; + echo '<table class="table table-bordered table-striped table-hover">'; + echo ''; + echo '
    Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
    Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
    Row 3, Cell 1Row 3, Cell 2Row 3, Cell 3
    Row 4, Cell 1Row 4, Cell 2Row 4, Cell 3
    Row 5, Cell 1Row 5, Cell 2Row 5, Cell 3
    Row 6, Cell 1Row 6, Cell 2Row 6, Cell 3
    Row 7, Cell 1Row 7, Cell 2Row 7, Cell 3
    '; + echo '
    '; // /.col-md-6 + + echo '
    '; // /.row + + echo '
    '; + + $this->page->non_admin_content_bootstrap = ob_get_clean(); + } + + + public function ThemeInfo($theme){ + + $template = dirname($theme); + $color = basename($theme); + + if( !isset($this->avail_addons[$template]) || + !isset($this->avail_addons[$template]['colors'][$color]) + ){ + return false; + } + + $theme_info = $this->avail_addons[$template]; + $theme_info['color'] = $color; + + return $theme_info; + } + + + /** + * Return an array of available themes + * @return array + * + */ + public function GetPossible(){ + + $this->avail_addons = []; + $this->versions = []; + + $this->AvailableThemes('/themes', false); //local themes + $this->AvailableThemes('/data/_themes', true); //downloaded themes + + //remove older versions + if( gp_unique_addons ){ + $themes = $this->avail_addons; + $this->avail_addons = []; + + foreach($themes as $index => $info){ + + if( !isset($info['id']) || !isset($info['version']) ){ + $this->avail_addons[$index] = $info; + continue; + } + + if( version_compare($this->versions[$info['id']]['version'], $info['version'], '>') ){ + continue; + } + + $this->avail_addons[$index] = $info; + } + + uksort($this->avail_addons, 'strnatcasecmp'); + } + } + + + /** + * Scan the directory for available themes + * + */ + private function AvailableThemes($dir_rel, $is_addon){ + global $dataDir; + + $dir = $dataDir . $dir_rel; + $folders = \gp\tool\Files::readDir($dir, 1); + + foreach($folders as $folder){ + + $addon = $this->AvailableTheme($dir_rel, $is_addon, $folder); + if( $addon === false ){ + continue; + } + + $index = $addon['index']; + $this->avail_addons[$index] = $addon; + } + } + + + /** + * Get info about one theme + * + */ + private function AvailableTheme($dir_rel, $is_addon, $folder){ + global $dataDir; + + $full_dir = $dataDir . '/' . $dir_rel . '/' . $folder; + $ini_info = $this->GetAvailInstall($full_dir); + + if( $ini_info === false ){ + return false; + } + + if( $is_addon ){ + $index = $ini_info['Addon_Name'] . '(remote)'; + }else{ + $index = $folder . '(local)'; + } + $this->AddVersionInfo($ini_info, $index); + + $addon = $this->IniExtract($ini_info); + + if( empty($addon['name']) ){ + $addon['name'] = $folder; + } + + $addon['folder'] = $folder; + $addon['colors'] = $this->GetThemeColors($full_dir); + $addon['is_addon'] = $is_addon; + $addon['full_dir'] = $full_dir; + $addon['rel'] = $dir_rel.'/'.$folder; + $addon['index'] = $index; + + return $addon; + } + + + /** + * Extract addon information from ini content + * + */ + public function IniExtract($ini_info){ + + $extracted = []; + + if( isset($ini_info['Addon_Unique_ID']) ){ + $extracted['addon_id'] = $ini_info['Addon_Unique_ID']; + } + + if( isset($ini_info['Addon_Version']) ){ + $extracted['version'] = $ini_info['Addon_Version']; + } + + if( isset($ini_info['Addon_Name']) ){ + $extracted['name'] = $ini_info['Addon_Name']; + } + + if( isset($ini_info['About']) ){ + $extracted['name'] = $ini_info['About']; + } + + return $extracted; + } + + + /** + * Keep track of theme versions + * + */ + private function AddVersionInfo($ini_info,$index){ + + if( isset($ini_info['Addon_Version']) && isset($ini_info['Addon_Unique_ID']) ){ + + $addon_id = $ini_info['Addon_Unique_ID']; + $version = $ini_info['Addon_Version']; + + if( !isset($this->versions[$addon_id]) ){ + $this->versions[$addon_id] = ['version' => $version, 'index' => $index]; + }elseif( version_compare($this->versions[$addon_id]['version'], $version, '<') ){ + $this->versions[$addon_id] = ['version' => $version, 'index' => $index]; + } + } + } + + + /** + * Return ini info if the addon is installable + * @return false|array + * + */ + public function GetAvailInstall($dir){ + global $langmessage; + + $iniFile = $dir . '/Addon.ini'; + $template_file = $dir . '/template.php'; + $dirname = basename($dir); + + if( !is_readable($dir) ){ + $this->invalid_folders[$dirname] = 'Directory is not readable'; + return false; + } + + if( !file_exists($template_file) ){ + $this->invalid_folders[$dirname] = 'template.php is not readable or does not exist'; + return false; + } + + if( !file_exists($iniFile) ){ + return []; + } + + $array = \gp\tool\Ini::ParseFile($iniFile); + if( $array === false ){ + return []; + } + + $array += ['Addon_Version' => '']; + + return $array; + } + + + /** + * Get a list of theme subfolders that have style.css files + * + */ + public function GetThemeColors($dir){ + $subdirs = \gp\tool\Files::readDir($dir, 1); + $colors = []; + + asort($subdirs); + + foreach($subdirs as $subdir){ + if( \gp\tool\Output::IsLayoutDir($dir . '/' . $subdir) !== false ){ + $colors[$subdir] = $subdir; + } + } + + return $colors; + } + + + /** + * Save $layout as the default layout for the site + * + */ + public function MakeDefault(){ + global $config; + + $config['gpLayout'] = $this->curr_layout; + + if( $this->SaveConfig() ){ + $this->page->SetTheme(); + $this->SetLayoutArray(); + } + } + + + /** + * Save the color and label of a layout + * + */ + public function LayoutLabel(){ + global $gpLayouts, $langmessage; + + $this->page->ajaxReplace = []; + + $layout = $this->ReqLayout(); + if( $layout === false ){ + return; + } + + if( !empty($_POST['color']) && + (strlen($_POST['color']) == 7) && + $_POST['color'][0] == '#' + ){ + $gpLayouts[$layout]['color'] = htmlspecialchars($_POST['color']); + } + + $gpLayouts[$layout]['label'] = htmlspecialchars($_POST['layout_label']); + + if( !$this->SaveLayouts(false) ){ + return; + } + + //send new label + $layout_info = \gp\tool::LayoutInfo($layout, false); + $replace = $this->GetLayoutLabel($layout, $layout_info); + $this->page->ajaxReplace[] = ['replace', '.layout_label_' . $layout, $replace]; + } + + + /** + * Display the color selector for + * @param string $layout The layout being edited + * + */ + public function ColorSelector($layout=false){ + global $langmessage; + + $colors = self::GetColors(); + echo '
    '; + echo '
    '; + + $form_action = ($layout === false) ? + \gp\tool::GetUrl('Admin_Theme_Content') : + \gp\tool::GetUrl('Admin_Theme_Content/Edit/' . $layout); + + echo '
    '; + + echo ''; + echo ''; + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo '
    '; + echo ' '; + echo ''; + echo '
    '; + echo '
    '; + foreach($colors as $color){ + echo ''; + echo ''; + } + echo '
    '; + echo '
    '; + echo ' '; + echo ' '; + echo '
    '; + echo '
    '; + echo '
    '; + echo '
    '; + } + + + /** + * Display layout label and options + * + */ + public function LayoutDiv($layout, $info){ + global $langmessage; + + $layout_info = \gp\tool::LayoutInfo($layout, false); + + echo '
    '; + echo $this->GetLayoutLabel($layout, $info); + + echo '
    '; + echo ''; + echo '
    '; + echo '
    '; + } + + + /** + * Output addon information about a layout + * + */ + public function LayoutDivAddon($layout_info){ + global $langmessage; + + // layouts with hooks + ob_start(); + $addon_config = false; + if( isset($layout_info['addon_key']) ){ + $addon_key = $layout_info['addon_key']; + $addon_config = \gp\tool\Plugins::GetAddonConfig($addon_key); + echo '
  • '; + echo \gp\tool::Link( + 'Admin/Addons/'.\gp\admin\Tools::encode64($addon_key), + ' ' . $addon_config['name'] + ); + echo '
  • '; + + //hooks + $this->AddonPanelGroup($addon_key, false); + } + + //version + if( !empty($layout_info['version']) ){ + echo '
  • ' . $langmessage['Your_version'] . ' ' . $layout_info['version'] . '
  • '; + }elseif( $addon_config && !empty($addon_config['version']) ){ + echo '
  • ' . $langmessage['Your_version'] . ' ' . $addon_config['version'] . '
  • '; + } + + //upgrade + if( $addon_config !== false ){ + echo '
  • '; + if( $layout_info['is_addon'] ){ + $source = $layout_info['name'] . '(remote)/' . $layout_info['theme_color']; + }else{ + $source = $layout_info['theme_name'] . '(local)/' . $layout_info['theme_color']; + } + echo \gp\tool::Link( + 'Admin_Theme_Content', + $langmessage['upgrade'], + 'cmd=UpgradeTheme&source=' . rawurlencode($source), + ['data-cmd' => 'creq'] + ); + echo '
  • '; + } + + $options = ob_get_clean(); + + if( !empty($options) ){ + echo '
  • '; + echo '' . $langmessage['options'] . ''; + echo '
      '; + + echo $options; + + echo '
  • '; + } + } + + + /** + * Return the layout name and id color + * + */ + public function GetLayoutLabel($layout, $layout_info){ + global $config, $langmessage, $config; + + ob_start(); + echo ''; + echo ''; + echo ' '; + echo ' '; + echo ' '; + if( $config['gpLayout'] == $layout ){ + echo ' (' . $langmessage['default'] . ')'; + echo ' '; + } + echo '' . $layout_info['label'] . ''; + + echo ''; + echo ''; + return ob_get_clean(); + } + + + /** + * Return form for name based menu classes and ordered menu classes + * + */ + public function CSSPreferenceForm($layout, $layout_info){ + global $langmessage; + + ob_start(); + echo '
      '; + + // name based menu classes + echo '
    • '; + echo '
      '; + echo ''; + echo ''; + $checked = ''; + $value = 'on'; + if( !isset($layout_info['menu_css_ordered']) ){ + $checked = ' checked="checked"'; + $value = 'off'; + } + echo ''; + echo ''; + echo '
      '; + echo '
    • '; + + //ordered menu classes + echo '
    • '; + echo '
      '; + echo ''; + echo ''; + $checked = ''; + $value = 'on'; + if( !isset($layout_info['menu_css_indexed']) ){ + $checked = ' checked="checked"'; + $value = 'off'; + } + echo ''; + echo ''; + echo '
      '; + echo '
    • '; + echo '
    '; + return ob_get_clean(); + } + + + public function ThemeLabel($theme_color){ + + $theme = $theme_color; + $color = false; + if( strpos($theme_color, '/') ){ + list($theme,$color) = explode('/', $theme_color); + } + + foreach($this->avail_addons as $info){ + if( $info['folder'] == $theme ){ + $theme = $info['name']; + break; + } + } + + if( $color ){ + return $theme . '/' . $color; + } + return $theme; + } + + + public function TitlesCount($layout){ + $titles_count = 0; + foreach($this->LayoutArray as $layout_comparison){ + if( $layout == $layout_comparison ){ + $titles_count++; + } + } + return $titles_count; + } + + + /** + * Restore a layout to it's default content arrangement + */ + public function RestoreLayout(){ + $this->SaveHandlersNew([], $this->curr_layout); + } + + + public function SaveHandlersNew($handlers, $layout=false){ + global $config, $langmessage, $gpLayouts; + + //make sure the keys are sequential + foreach($handlers as $container => $container_info){ + if( is_array($container_info) ){ + $handlers[$container] = array_values($container_info); + } + } + + if( $layout === false ){ + $layout = $this->curr_layout; + } + + if( !isset($gpLayouts[$layout]) ){ + msg($langmessage['OOPS']); + return false; + } + + if( count($handlers) === 0 ){ + unset($gpLayouts[$layout]['handlers']); + }else{ + $gpLayouts[$layout]['handlers'] = $handlers; + } + + $this->SaveLayouts(); + } + + + public function GetAllHandlers($layout=false){ + global $gpLayouts, $config; + + if( $layout === false ){ + $layout = $this->curr_layout; + } + + $handlers =& $gpLayouts[$layout]['handlers']; + + if( !is_array($handlers) || count($handlers) < 1 ){ + $gpLayouts[$layout]['hander_v'] = '2'; + $handlers = []; + } + + //clean : characters for backwards compat + foreach($handlers as $container => $container_info){ + if( is_string($container_info) ){ + $handlers[$container] = trim($container_info, ':'); + continue; + } + if( !is_array($container_info) ){ + continue; + } + foreach($container_info as $key => $gpOutCmd){ + $handlers[$container][$key] = trim($gpOutCmd, ':'); + } + } + + return $handlers; + } + + + //set default values if not set + public function PrepContainerHandlers(&$handlers, $container, $gpOutCmd){ + if( isset($handlers[$container]) && is_array($handlers[$container]) ){ + return; + } + $handlers[$container] = $this->GetDefaultList($container, $gpOutCmd); + } + + + public function GetDefaultList($container, $gpOutCmd){ + global $config; + + if( $container !== 'GetAllGadgets' ){ + //Just a container that doesn't have content by default + // ex: \gp\tool\Output::Get('AfterContent'); + if( empty($gpOutCmd) ){ + return []; + } + return [$gpOutCmd]; + } + + $result = []; + if( isset($config['gadgets']) && is_array($config['gadgets']) ){ + foreach($config['gadgets'] as $gadget => $info){ + if( isset($info['addon']) ){ + $result[] = $gadget; + } + } + } + + return $result; + } + + + public function ReturnHeader(){ + global $page; + + $page->ajaxReplace = []; + $page->ajaxReplace[] = ['reload']; + } + + + public function SetLayoutArray(){ + global $gp_menu, $gp_titles, $gp_index, $config; + + $titleThemes = []; + $customThemes = []; + $max_level = 5; + + foreach($gp_menu as $id => $info){ + + $level = $info['level']; + + //reset theme inheritance + $max_level = max($max_level, $level); + for( $i = $level; $i <= $max_level; $i++){ + if( isset($customThemes[$i]) ){ + $customThemes[$i] = false; + } + } + + if( !empty($gp_titles[$id]['gpLayout']) ){ + + $titleThemes[$id] = $gp_titles[$id]['gpLayout']; + + }else{ + + $parent_layout = false; + $temp_level = $level; + while( $temp_level >= 0 ){ + if( isset($customThemes[$temp_level]) && ($customThemes[$temp_level] !== false) ){ + $titleThemes[$id] = $parent_layout = $customThemes[$temp_level]; + break; + } + $temp_level--; + } + + if( $parent_layout === false ){ + $titleThemes[$id] = $config['gpLayout']; + } + } + + $customThemes[$level] = $titleThemes[$id]; + } + + foreach($gp_index as $title => $id){ + $titleInfo = $gp_titles[$id]; + + if( isset($titleThemes[$id]) ){ + continue; + } + + if( !empty($titleInfo['gpLayout']) ){ + $titleThemes[$id] = $titleInfo['gpLayout']; + }else{ + $titleThemes[$id] = $config['gpLayout']; + } + } + + $this->LayoutArray = $titleThemes; + } + + + /** + * Remove a layout + * + */ + public function DeleteLayout(){ + global $gpLayouts, $langmessage, $gp_titles; + + $layout =& $_POST['layout']; + if( !isset($gpLayouts[$layout]) ){ + msg($langmessage['OOPS'] . ' (Layout not set)'); + return false; + } + + //remove from $gp_titles + $this->RmLayout($layout); + } + + + /** + * Remove a layout from $gp_titles and $gpLayouts + * + */ + public function RmLayout($layout){ + global $gp_titles, $gpLayouts, $langmessage; + + $this->RmLayoutPrep($layout); + + //determine if code in /data/_themes/$layout/ should be removed + $rm_addon = $this->RemoveAddonCode($layout); + + unset($gpLayouts[$layout]); + + //delete and save + if( $rm_addon ){ + $installer = new \gp\admin\Addon\Installer(); + $installer->rm_folders = false; + if( !$installer->Uninstall($rm_addon) ){ + $gpLayouts = $this->gpLayouts_before; + } + $installer->OutputMessages(); + } + + if( !$this->SaveLayouts() ){ + return false; + } + + //remove custom layout files and its directory + $this->RemoveCustomDir($layout); + } + + + /** + * Determine if the code in /data/_themes/$layout/ should be removed + * + */ + public function RemoveAddonCode($layout){ + global $gpLayouts; + + + if( !isset($gpLayouts[$layout]['addon_key']) ){ + return false; + } + + $rm_addon = $gpLayouts[$layout]['addon_key']; + + //don't remove if there are other layouts using the same code + foreach($gpLayouts as $layout_id => $info){ + if( $layout_id == $layout ){ + continue; + } + if( !array_key_exists('addon_key', $info) ){ + continue; + } + if( $info['addon_key'] == $rm_addon ){ + $rm_addon = false; + } + } + + return $rm_addon; + } + + + public function RmLayoutPrep($layout){ + global $gp_titles; + + //remove from $gp_titles + foreach($gp_titles as $title => $titleInfo){ + + if( empty($titleInfo['gpLayout']) ){ + continue; + } + + if( $titleInfo['gpLayout'] == $layout ){ + unset($gp_titles[$title]['gpLayout']); + } + } + } + + + public function LayoutUrl($layout, &$query=''){ + $url = 'Admin_Theme_Content'; + if( $this->layout_request ){ + $url = 'Admin_Theme_Content/Edit/' . rawurlencode($layout); + }else{ + $query .= '&layout=' . rawurlencode($layout); + } + return $url; + } + + + public function LayoutLink($layout, $label, $query, $attr){ + $url = $this->LayoutUrl($layout, $query); + return \gp\tool::Link($url, $label, $query, $attr); + } + + + /** + * Get the requested layout + * + */ + public function ReqLayout(){ + global $langmessage, $gpLayouts; + + if( !isset($_REQUEST['layout']) || !isset($gpLayouts[$_REQUEST['layout']]) ){ + msg($langmessage['OOPS'] . '(Invalid layout)'); + return; + } + + return $_REQUEST['layout']; + } + +} diff --git a/include/admin/Layout/Available.php b/include/admin/Layout/Available.php new file mode 100644 index 0000000..d4b000f --- /dev/null +++ b/include/admin/Layout/Available.php @@ -0,0 +1,701 @@ +page->head_js[] = '/include/js/auto_width.js'; + } + + + /** + * Show available themes and style variations + * + */ + public function ShowAvailable(){ + + $cmd = \gp\tool::GetCommand(); + + switch($cmd){ + case 'preview': + case 'preview_iframe': + case 'newlayout': + case 'addlayout': + if( $this->NewLayout($cmd) ){ + return; + } + break; + + case 'DeleteTheme': + $this->DeleteTheme(); + $this->GetPossible(); + break; + } + + $this->GetAddonData(); + + $this->ShowHeader(); + + $this->AvailableList(); + + $this->InvalidFolders(); + } + + + public function AvailableList( $show_options = true ){ + global $langmessage, $config; + + //search settings + $this->searchPerPage = 10; + $this->searchOrderOptions = []; + $this->searchOrderOptions['modified'] = $langmessage['Recently Updated']; + $this->searchOrderOptions['rating_score'] = $langmessage['Highest Rated']; + $this->searchOrderOptions['downloads'] = $langmessage['Most Downloaded']; + + $this->SearchOrder(); + $this->SortAvailable(); + + // pagination + $this->searchMax = count($this->avail_addons); + $this->searchPage = \gp\special\Search::ReqPage('page', $this->searchMax); + + $start = $this->searchPage * $this->searchPerPage; + $possible = array_slice($this->avail_addons, $start, $this->searchPerPage, true); + + if( $show_options ){ + $this->SearchOptions(); + echo '
    '; + } + + // show themes + echo '
    '; + foreach($possible as $theme_id => $info){ + $this->AvailableTheme($theme_id, $info, $show_options); + } + echo '
    '; + + if( $show_options ){ + $this->SearchNavLinks(); + } + } + + + protected function AvailableTheme($theme_id, $info, $show_options){ + global $langmessage, $config; + + $theme_label = str_replace('_', ' ', $info['name']); + $version = ''; + $id = false; + if( isset($info['version']) ){ + $version = $info['version']; + } + if( isset($info['addon_id']) && is_numeric($info['addon_id']) ){ + $id = $info['addon_id']; + } + + //screenshot + if( file_exists($info['full_dir'] . '/screenshot.png') ){ + + $screenshot_url = \gp\tool::GetDir($info['rel'] . '/screenshot.png'); + echo '
    '; + echo '' . $theme_label . ' ' . $version . ''; + echo '
    '; + + }elseif( file_exists($info['full_dir'] . '/screenshot.jpg') ){ + + $screenshot_url = \gp\tool::GetDir($info['rel'] . '/screenshot.jpg'); + echo '
    '; + echo '' . $theme_label . ' ' . $version . ''; + echo '
    '; + + }else{ + + echo '
    '; + echo '' . $theme_label . ' ' . $version . ''; + echo '
    '; + + } + + //options + echo '
    '; + + //colors + if( $show_options ){ + $color_q = 'cmd=preview' . $this->searchQuery; + $color_a = ''; + }else{ + $color_q = 'cmd=preview_iframe'; + $color_a = ' target="gp_layout_iframe" data-cmd="SetPreviewTheme" '; + } + + echo '' . $langmessage['preview'] . ''; + echo '
      '; + foreach($info['colors'] as $color){ + echo '
    • '; + + $theme = $theme_id . '/' . $color; + $q = $color_q . '&theme=' . rawurlencode($theme); + $a = $color_a . ' data-arg="' . htmlspecialchars($theme) . '"'; + + echo \gp\tool::Link( + 'Admin_Theme_Content/Available', + str_replace('_', ' ', $color), + $q, + $a + ); + echo '
    • '; + } + echo '
    '; + + $options = $this->AvailableThemeOptions($id, $info, $theme_label); + + if( !empty($options) ){ + echo '' . $langmessage['options'] . ''; + echo '
      '; + echo $options; + echo '
    '; + } + + echo '
    '; // /.gp_theme_options + + echo '
    '; + + //remote upgrade + if( gp_remote_themes && + $id && + isset(\gp\admin\Tools::$new_versions[$id]) && + version_compare(\gp\admin\Tools::$new_versions[$id]['version'], $version ,'>') + ){ + $version_info = \gp\admin\Tools::$new_versions[$id]; + echo \gp\tool::Link( + 'Admin_Theme_Content', + $langmessage['new_version'], + 'cmd=RemoteInstall&id=' . $id . '&name=' . rawurlencode($version_info['name']) + ); + } + + echo '
    '; + } + + + protected function AvailableThemeOptions($id, $info, $theme_label){ + global $langmessage, $config; + + ob_start(); + + if( $id ){ + //more info + echo '
  • '; + echo self::DetailLink('theme', $id, 'More Info…'); + echo '
  • '; + + //support + $forum_id = 1000 + $id; + echo '
  • '; + echo ''; + echo $langmessage['Support']; + echo ''; + echo '
  • '; + + //rating + $rating = 0; + if( $info['rt'] > 0 ){ + $rating = $info['rt']; + } + echo '
  • '; + echo ''; + echo $langmessage['rate'] . ' '; + echo $this->ShowRating($info['rel'], $rating); + echo ''; + echo '
  • '; + + //downloads + if( $info['dn'] > 0 ){ + echo '
  • '; + echo ''; + echo 'Downloads: ' . number_format($info['dn']); + echo ''; + echo '
  • '; + } + } + + //last updated + if( $info['tm'] > 0 ){ + echo '
  • '; + echo ''; + echo $langmessage['Modified'] . ': '; + echo \gp\tool::date($langmessage['strftime_datetime'], $info['tm']); + echo ''; + echo '
  • '; + } + + if( $info['is_addon'] ){ + //delete + $folder = $info['folder']; + $title = sprintf($langmessage['generic_delete_confirm'], $theme_label); + $attr = [ + 'data-cmd' => 'cnreq', + 'class' => 'gpconfirm', + 'title' => $title, + ]; + echo '
  • '; + echo \gp\tool::Link( + 'Admin_Theme_Content/Available', + $langmessage['delete'], + 'cmd=DeleteTheme&folder=' . rawurlencode($folder), + $attr + ); + echo '
  • '; + + //order + if( isset($config['themes'][$folder]['order']) ){ + echo '
  • '; + echo 'Order: ' . $config['themes'][$folder]['order']; + echo '
  • '; + } + } + + return ob_get_clean(); + } + + + /** + * Sort the list available addons + * + */ + private function SortAvailable(){ + + // get addon information for ordering + \gp\admin\Tools::VersionData($version_data); + $version_data = $version_data['packages']; + + // combine remote addon information + foreach($this->avail_addons as $theme_id => $info){ + + if( isset($info['id']) ){ + $id = $info['id']; + + if( isset($version_data[$id]) ){ + $info = array_merge($info, $version_data[$id]); + $info['rt'] *= 5; + } + + // use local rating + if( isset($this->addonReviews[$id]) ){ + $info['rt'] = $this->addonReviews[$id]['rating']; + } + }else{ + // give local themes a high rating to make them appear first + // rating won't actually display + $info['rt'] = 6; + } + + $info += ['dn' => 0, 'rt' => 0]; + + //modified time + if( !isset($info['tm']) ){ + $info['tm'] = self::ModifiedTime( $info['full_dir'] ); + } + + $this->avail_addons[$theme_id] = $info; + } + + // sort by + uasort($this->avail_addons, [$this, 'SortUpdated']); + switch($this->searchOrder){ + + case 'downloads': + uasort($this->avail_addons, [$this, 'SortDownloads']); + break; + + case 'modified': + uasort($this->avail_addons, [$this, 'SortRating']); + uasort($this->avail_addons, [$this, 'SortUpdated']); + break; + + case 'rating_score': + default: + uasort($this->avail_addons, [$this, 'SortRating']); + break; + } + } + + + public static function ModifiedTime($directory){ + + $files = scandir( $directory ); + $time = filemtime( $directory ); + + foreach($files as $file){ + if( $file == '..' || $file == '.' ){ + continue; + } + + $full_path = $directory . '/' . $file; + + if( is_dir($full_path) ){ + $time = max($time, self::ModifiedTime($full_path)); + }else{ + $time = max($time, filemtime($full_path)); + } + } + + return $time; + } + + + public function SortDownloads($a, $b){ + return $b['dn'] > $a['dn']; + } + + + public function SortRating($a, $b){ + return $b['rt'] > $a['rt']; + } + + + public function SortUpdated($a, $b){ + return $b['tm'] > $a['tm']; + } + + + /** + * Manage adding new layouts + * + */ + public function NewLayout($cmd){ + global $langmessage; + + //check the requested theme + $theme =& $_REQUEST['theme']; + $theme_info = $this->ThemeInfo($theme); + + if( $theme_info === false ){ + msg($langmessage['OOPS'] . ' (Invalid Theme)'); + return false; + } + + // three steps of installation + switch($cmd){ + case 'preview': + if( $this->PreviewTheme($theme, $theme_info) ){ + return true; + } + break; + + case 'preview_iframe': + $this->PreviewThemeIframe($theme, $theme_info); + return true; + + case 'newlayout': + $this->NewLayoutPrompt($theme, $theme_info); + return true; + + case 'addlayout': + $this->AddLayout($theme_info); + break; + } + + return false; + } + + + /** + * Preview a theme and give users the option of creating a new layout + * + */ + public function PreviewTheme($theme, $theme_info){ + global $langmessage, $config; + + //force showing only the body as a complete html document + $_REQUEST += ['gpreq' => 'body']; + + \gp\admin\Tools::$show_toolbar = false; + $this->page->get_theme_css = false; + $this->page->head_js[] = '/include/js/auto_width.js'; + $this->page->head_js[] = '/include/js/theme_content_outer.js'; + $this->page->head_js[] = '/include/js/preview_themes.js'; + $this->page->css_admin[] = '/include/css/theme_content_outer.scss'; + + $iframe_src = \gp\tool::GetUrl( + 'Admin_Theme_Content/Available', + 'cmd=preview_iframe&theme=' . rawurlencode($theme) + ); + + //show site in iframe + echo '
    '; + echo ''; + echo '
    '; // /#gp_iframe_wrap + + ob_start(); + + //new + echo '
    '; + echo '
    '; + + echo '
    '; + echo \gp\tool::Link( + 'Admin_Theme_Content/Available', + '« ' . $langmessage['available_themes'] + ); + echo \gp\tool::Link( + 'Admin_Theme_Content/Available', + $langmessage['use_this_theme'], + 'cmd=newlayout&theme=' . rawurlencode($theme), + ['data-cmd' => 'gpabox', 'class' => 'add_layout'] + ); + echo '
    '; + + echo '
    '; + echo '
    '; + + $this->searchUrl = 'Admin_Theme_Content/Available'; + $this->AvailableList(false); + + //search options + $this->searchQuery .= '&cmd=preview&theme=' . rawurlencode($theme); + $this->SearchOptions(false); + + echo '
    '; // /.gp_scroll_area + echo '
    '; // /#theme_editor + + echo '
    '; + echo '
    '; + + $this->page->admin_html = ob_get_clean(); + + return true; + } + + + public function PreviewThemeIframe($theme, $theme_info){ + global $langmessage, $config; + + \gp\admin\Tools::$show_toolbar = false; + + $this->page->gpLayout = false; + $this->page->theme_name = $theme_info['folder']; + $this->page->theme_color = $theme_info['color']; + $this->page->theme_dir = $theme_info['full_dir']; + $this->page->theme_rel = $theme_info['rel'] . '/' . $theme_info['color']; + + $this->LoremIpsum(); + + if( isset($theme_info['id']) ){ + $this->page->theme_addon_id = $theme_info['id']; + } + + $this->page->theme_path = \gp\tool::GetDir($this->page->theme_rel); + + $this->page->show_admin_content = false; + } + + + /** + * Give users a few options before creating the new layout + * + */ + public function NewLayoutPrompt($theme, $theme_info ){ + global $langmessage; + + $label = substr($theme_info['name'] . '/' . $theme_info['color'], 0, 25); + + echo '

    ' . $langmessage['new_layout'] . '

    '; + + echo '
    '; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
    '; + echo $langmessage['options']; + echo '
    '; + echo $langmessage['label']; + echo ''; + echo ''; + echo '
    '; + echo $langmessage['make_default']; + echo ''; + echo ''; + echo '
    '; + + echo '

    '; + echo ' '; + echo ' '; + echo ' '; + echo '

    '; + + echo '
    '; + } + + + /** + * Add a new layout to the installation + * + */ + public function AddLayout($theme_info){ + global $gpLayouts, $langmessage, $config; + + $new_layout = []; + $new_layout['theme'] = $theme_info['folder'] . '/' . $theme_info['color']; + $new_layout['color'] = self::GetRandColor(); + $new_layout['label'] = htmlspecialchars($_POST['label']); + + $customizer_file = $theme_info['full_dir'] . '/' . $theme_info['color'] . '/customizer.php'; + $new_layout['config'] = \gp\tool\Output::GetCustomizerDefaults('php', $customizer_file); + $js_vars = \gp\tool\Output::GetCustomizerDefaults('js', $customizer_file); + $js_vars = "\n" . 'var layout_config = ' . json_encode($js_vars) . ';' . "\n"; + $new_layout['js_vars'] = $js_vars; + + if( $theme_info['is_addon'] ){ + $new_layout['is_addon'] = true; + } + + $installer = new \gp\admin\Addon\Installer(); + $installer->addon_folder_rel = dirname($theme_info['rel']); + $installer->code_folder_name = '_themes'; + $installer->source = $theme_info['full_dir']; + $installer->new_layout = $new_layout; + + if( !empty($_POST['default']) && $_POST['default'] != 'false' ){ + $installer->default_layout = true; + } + + $success = $installer->Install(); + $installer->OutputMessages(); + + if( $success && $installer->default_layout ){ + $this->page->SetTheme(); + $this->SetLayoutArray(); + } + } + + + /** + * Delete a remote theme + * + */ + public function DeleteTheme(){ + global $langmessage, $dataDir, $gpLayouts, $config; + + $config_before = $config; + $gpLayoutsBefore = $gpLayouts; + $theme_folder_name =& $_POST['folder']; + + if( empty($theme_folder_name) || !ctype_alnum($theme_folder_name) ){ + msg($langmessage['OOPS'] . ' (Invalid Request)'); + return false; + } + + $order = false; + if( isset($config['themes'][$theme_folder_name]['order']) ){ + $order = $config['themes'][$theme_folder_name]['order']; + } + + if( !$this->CanDeleteTheme($theme_folder_name, $message) ){ + msg($message); + return false; + } + + //remove layouts + $rm_addon = false; + foreach($gpLayouts as $layout_id => $layout_info){ + + if( !isset($layout_info['is_addon']) || !$layout_info['is_addon'] ){ + continue; + } + + $layout_folder = dirname($layout_info['theme']); + if( $layout_folder != $theme_folder_name ){ + continue; + } + + if( array_key_exists('addon_key', $layout_info) ){ + $rm_addon = $layout_info['addon_key']; + } + + $this->RmLayoutPrep($layout_id); + unset($gpLayouts[$layout_id]); + } + + + //remove from settings + unset($config['themes'][$theme_folder_name]); + + if( $rm_addon ){ + + $installer = new \gp\admin\Addon\Installer(); + if( !$installer->Uninstall($rm_addon) ){ + $gpLayouts = $gpLayoutsBefore; + } + $installer->OutputMessages(); + + }else{ + + if( !\gp\admin\Tools::SaveAllConfig() ){ + $config = $config_before; + $gpLayouts = $gpLayoutsBefore; + msg($langmessage['OOPS'] . ' (s1)'); + return false; + } + + msg($langmessage['SAVED']); + if( $order ){ + $img_path = \gp\tool::IdUrl('ci'); + \gp\tool::IdReq($img_path); + } + + } + + //delete the folder if it hasn't already been deleted by addon installer + $dir = $dataDir . '/data/_themes/' . $theme_folder_name; + if( file_exists($dir) ){ + \gp\tool\Files::RmAll($dir); + } + } + + + public function CanDeleteTheme($folder, &$message){ + global $gpLayouts, $config, $langmessage; + + foreach($gpLayouts as $layout_id => $layout){ + + if( !isset($layout['is_addon']) || !$layout['is_addon'] ){ + continue; + } + + $layout_folder = dirname($layout['theme']); + if( $layout_folder == $folder ){ + if( $config['gpLayout'] == $layout_id ){ + $message = $langmessage['delete_default_layout']; + return false; + } + } + } + return true; + } + +} diff --git a/include/admin/Layout/Edit.php b/include/admin/Layout/Edit.php new file mode 100644 index 0000000..fbdcea2 --- /dev/null +++ b/include/admin/Layout/Edit.php @@ -0,0 +1,2149 @@ +page->requested); + + // prevent opening layout editor by locked users + if( isset($gpAdmin['locked']) && $gpAdmin['locked'] ){ + $url = \gp\tool::GetUrl(isset($_REQUEST['redir']) ? $_REQUEST['redir'] : 'Admin'); + \gp\tool::Redirect($url); + } + + if( !empty($parts[2]) ){ + + if( $this->SetCurrLayout($parts[2]) ){ + $this->EditLayout(); + return; + } + + //default layout + }elseif( $this->SetCurrLayout($config['gpLayout']) ){ + $this->EditLayout(); + return; + } + + //redirect + $url = \gp\tool::GetUrl('Admin_Theme_Content', '', false); + \gp\tool::Redirect($url, 302); + } + + + /** + * Set the current layout + * + */ + protected function SetCurrLayout($layout){ + global $langmessage, $gpLayouts; + + if( !isset($gpLayouts[$layout]) ){ + return false; + } + + $this->curr_layout = $layout; + $this->SetLayoutArray(); + $this->page->SetTheme($layout); + + if( !$this->page->gpLayout ){ + msg($langmessage['OOPS'] . ' (Theme Not Found)'); + parent::RunScript(); + return false; + } + + \gp\tool\Output::TemplateSettings(); + + return true; + } + + + /** + * Edit layout properties + * Layout Identification + * Content Arrangement + * Gadget Visibility + * + */ + public function EditLayout(){ + + $GLOBALS['GP_ARRANGE_CONTENT'] = true; + $this->layout_slug = 'Admin_Theme_Content/Edit/' . rawurlencode($this->curr_layout); + + $this->cmds['ShowThemeImages'] = ''; + $this->cmds['SelectContent'] = ''; + + $this->cmds['LayoutMenu'] = ''; + $this->cmds['LayoutMenuSave'] = 'ReturnHeader'; + + //show the layout (displayed within an iframe) + $this->cmds['SaveChanges'] = 'ShowInIframe'; + $this->cmds['PreviewChanges'] = 'ShowInIframe'; + $this->cmds['addcontent'] = 'ShowInIframe'; + $this->cmds['RemoveArea'] = 'ShowInIframe'; + $this->cmds['DragArea'] = 'ShowInIframe'; + $this->cmds['in_ifrm'] = 'ShowInIframe'; + + \gp\tool\Plugins::Action('edit_layout_cmd', [$this->curr_layout]); + + $cmd = \gp\tool::GetCommand(); + + $this->LayoutCommands(); + $this->RunCommands($cmd); + } + + + public function DefaultDisplay(){ + global $langmessage; + + $layout_info = \gp\tool::LayoutInfo($this->curr_layout, false); + $this->page->label = $langmessage['layouts'] . ' » ' . $layout_info['label']; + + $this->LayoutEditor($this->curr_layout, $layout_info); + } + + + /** + * Prepare the page for css editing + * + */ + public function ShowInIframe(){ + + $this->LoremIpsum(); + + $cmd = \gp\tool::GetCommand(); + + $this->page->show_admin_content = false; + \gp\admin\Tools::$show_toolbar = false; + + // + $this->page->head .= ''; + if( $cmd != 'PreviewCSS' ){ + $this->page->head .= ''; + } + } + + + /** + * Display the toolbar for layout editing + * + */ + public function LayoutEditor($layout, $layout_info){ + global $langmessage, $gpAdmin; + + $_REQUEST += ['gpreq' => 'body']; //force showing only the body as a complete html document + $this->page->get_theme_css = false; + \gp\admin\Tools::$show_toolbar = false; + + \gp\tool::LoadComponents('colorpicker'); + + $this->page->css_user[] = '/include/thirdparty/codemirror/lib/codemirror.css'; + $this->page->head_js[] = '/include/thirdparty/codemirror/lib/codemirror.js'; + $this->page->head_js[] = '/include/thirdparty/codemirror/mode/css/css.js'; + $this->page->head_js[] = '/include/thirdparty/codemirror/addon/display/placeholder.js'; + + $this->page->css_admin[] = '/include/css/theme_content_outer.scss'; + $this->page->head_js[] = '/include/js/theme_content_outer.js'; + $this->page->head_js[] = '/include/js/layout_editor.js'; + + //custom css + $css = $this->GetLayoutCSS($this->curr_layout); + $dir = $layout_info['dir'] . '/' . $layout_info['theme_color']; + $style_type = \gp\tool\Output::StyleType($dir); + + $style_type_info = []; + switch($style_type){ + case 'scss': + $style_type_info['name'] = 'Scss'; + $style_type_info['link'] = 'https://sass-lang.com/'; + break; + + case 'less': + $style_type_info['name'] = 'Less'; + $style_type_info['link'] = 'http://lesscss.org/'; + break; + + case 'css': + default: + $style_type_info['name'] = 'CSS'; + $style_type_info['link'] = 'https://developer.mozilla.org/docs/Web/CSS'; + } + + //Iframe + echo '
    '; + $url = \gp\tool::GetUrl('Admin_Theme_Content/Edit/' . rawurlencode($layout), 'cmd=in_ifrm'); + echo ''; + echo '
    '; + + //CSS Editing + ob_start(); + $form_action = \gp\tool::GetUrl( + 'Admin_Theme_Content/Edit/' . $this->curr_layout, 'cmd=in_ifrm' + ); + echo '
    '; + echo '
    '; + + //selects + echo '
    '; + + //layout + echo '
    '; + $this->LayoutSelect($layout, $layout_info); + echo '
    '; + + //options + echo '
    '; + echo '
    '; + echo '' . $langmessage['Layout Options'] . ''; + echo '
    '; + echo '
      '; + $this->LayoutOptions($layout, $layout_info); + echo '
    '; + echo '
    '; + echo '
    '; + echo '
    '; + + echo '
    '; + + //editor/customizer tabs + echo '
    '; + + $active_class = ' active'; // first tab is active + + + //customizer tab + $this->customizer_data = $this->GetLayoutCustomizer($layout); + $this->layout_config = $this->GetLayoutConfig($layout); + + if( !empty($this->customizer_data) ){ + echo ''; + echo ''; + echo $langmessage['Customizer']; + echo ''; + $active_class = ''; + } + + //css editor tab + echo ''; + echo ''; + echo $style_type_info['name']; + echo ''; + $active_class = ''; + + echo '
    '; + + + //editor/customizer area (flex-grows) + echo '
    '; + + if( !empty($this->customizer_data) ){ + echo '
    '; + echo '
    '; + $this->Customizer(); + echo '
    '; + echo '
    ';// /.customizer_area + } + + //editor area (flex-grows) + echo '
    '; + if( empty($css) ){ + $var_file = $dir . '/variables.' . $style_type; + if( file_exists($var_file) ){ + $css = file_get_contents($var_file); + } + } + + $style_type_hint = $style_type != 'css' ? + strtoupper($style_type) . ' / CSS' : + strtoupper($style_type); + + $placeholder = sprintf($langmessage['Add X here'], $style_type_hint); + + echo ''; + // info link + echo ''; + echo ''; + echo ''; + + echo '
    ';// /.css_editor_area + + echo '
    ';// /.full_height + + + //hidden inputs to indicate which parts should be saved + echo ''; + echo ''; + + //buttons + echo '
    '; + + //preview + echo ''; + + //save + echo ''; + + //reset + echo ''; + echo $langmessage['Reset']; + echo ''; + + //cancel + $cancel_url = !empty($_REQUEST['redir']) ? $_REQUEST['redir'] : 'Admin_Theme_Content'; + echo \gp\tool::Link( + $cancel_url, + $langmessage['Close'], + '', + 'class="gpcancel"' + ); + + echo '
    '; // /.css_buttons + + echo '
    '; + + echo '
    '; // /#theme_editor + + $this->page->admin_html = ob_get_clean(); + } + + + /** + * Display all the layouts available in a '; + foreach( $control['units'] as $key => $units ){ + $option_text = !is_numeric($key) ? $key : $units; + $selected = $current_units == $units ? ' selected="selected"' : ''; + echo ''; + } + echo ''; + } + echo '
    '; // /.customizer_input_group + break; + + case 'select': + if( !empty($control['description']) ){ + echo '
    '; + echo htmlspecialchars($control['description']); + echo '
    '; + } + echo '
    '; + echo ''; + foreach( $control['possible_values'] as $key => $value ){ + is_bool($current_value) && $current_value = $current_value ? 'on' : 'off'; + $option_text = !is_numeric($key) ? $key : $value; + $selected = $current_value == $value ? ' selected="selected"' : ''; + echo ''; + } + echo ''; + echo '
    '; // /.customizer_input_group + break; + + case 'checkbox': + echo '
    '; + + $checked = $current_value ? ' checked="checked"' : ''; + $on_off = $current_value ? 'on' : 'off'; + $id = 'checkbox_' . $value_name; + + echo ''; + echo ''; + echo ''; + echo '
    '; // /.customizer_checkbox_group + break; + + case 'radio': + if( !empty($control['description']) ){ + echo '
    '; + echo htmlspecialchars($control['description']); + echo '
    '; + } + echo '
    '; + $i = 0; + foreach( $control['possible_values'] as $key => $value ){ + $radio_label = !is_numeric($key) ? $key : $value; + $checked = $current_value == $value ? ' checked="checked"' : ''; + $id = 'radio_' . $value_name . '_' . $i; + echo ''; + echo ''; + $i++; + } + echo '
    '; // /.customizer_radio_group + break; + + case 'colorpicker': + if( !empty($control['description']) ){ + echo '
    '; + echo htmlspecialchars($control['description']); + echo '
    '; + } + echo '
    '; + echo ''; + echo '
    '; // /.customizer_input_group + break; + + case 'colors': + if( !empty($control['description']) ){ + echo '
    '; + echo htmlspecialchars($control['description']); + echo '
    '; + } + + echo '
    '; + $i = 0; + foreach( $control['possible_values'] as $key => $value ){ + $color_title = !is_numeric($key) ? $key : $value; + $checked = $current_value == $value ? ' checked="checked"' : ''; + $id = 'color_' . $value_name . '_' . $i; + + echo ''; + echo ''; + echo ''; + echo ''; + $i++; + } + // add unset swatch + $checked = $current_value == '' ? ' checked="checked"' : ''; + $id = 'color_disabled_' . $value_name; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
    '; // /.customizer_colors_group + break; + + case 'file': + case 'image': + if( !empty($control['description']) ){ + echo '
    '; + echo htmlspecialchars($control['description']); + echo '
    '; + } + echo '
    '; + + $is_image = preg_match( + '/\.(jpg|jpeg|png|apng|gif|webp|svg|bmp|ico)$/i', + $current_value + ); + $display_class = $is_image ? '' : ' nodisplay'; + $img_src = $is_image ? ' src="' . $current_value . '"' : ''; + + echo ''; + echo ''; + echo ''; // /.customizer_image_preview + + $title = $langmessage['uploaded_files']; + $icon = 'fa-file-o'; + if( $control['type'] == 'image' ){ + $title = $langmessage['Select Image']; + $fa_icon = 'fa-image'; + } + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
    '; // /.customizer_input_group + + echo '
    '; // /.customizer_file_group + break; + } + + echo '
    '; // /.customizer_control + } + + + /** + * Save changes made in the Layout Editor + * + */ + public function SaveChanges(){ + + // only if dirty + if( !empty($_POST['save_customizer']) ){ + $this->SaveCustomizer(); + } + + // only if dirty + if( !empty($_POST['save_css']) ){ + $this->SaveCSS(); + } + } + + + /** + * Save Customizer values + * + */ + public function SaveCustomizer(){ + global $gpLayouts; + + $this->CustomizerResults(); + // msg('CustomizerResults() → $this->customizer_results = ' . pre($this->customizer_results)); + + // if( !empty($this->customizer_results['js_vars']) ){ + // $this->page->head_script .= $this->customizer_results['js_vars']; + // } + + if( !$this->SaveCustomizerResults($this->curr_layout, $this->customizer_results) ){ + return false; + } + + $gpLayouts[$this->curr_layout]['customizer_css'] = true; + + // js vars + if( empty($this->customizer_results['js_vars']) ){ + unset($gpLayouts[$this->curr_layout]['js_vars']); + }else{ + $gpLayouts[$this->curr_layout]['js_vars'] = $this->customizer_results['js_vars']; + } + + // php vars + if( empty($this->customizer_results['php_vars']) ){ + unset($gpLayouts[$this->curr_layout]['config']); + }else{ + $gpLayouts[$this->curr_layout]['config'] = $this->customizer_results['php_vars']; + } + + // save + if( !$this->SaveLayouts() ){ + return false; + } + + return true; + } + + + /** + * Save edits to the layout css + * + */ + public function SaveCSS(){ + global $gpLayouts; + + $css =& $_POST['css']; + + if( !$this->SaveCustomCSS($this->curr_layout, $css) ){ + return false; + } + + $gpLayouts[$this->curr_layout]['css'] = true; + if( !$this->SaveLayouts() ){ + return false; + } + $this->page->SetTheme($this->curr_layout); + } + + + /** + * Preview changes made in the Layout Editor + * + */ + public function PreviewChanges(){ + $this->PreviewCustomizer(); + $this->PreviewCSS(); + } + + + /** + * set current theme/laout values + * + */ + public function SetLayoutValues(){ + $this->page->layout_info = \gp\tool::LayoutInfo($this->curr_layout, false); + $this->page->theme_color = $this->page->layout_info['theme_color']; + $this->page->theme_rel = dirname($this->page->theme_rel) . '/' . $this->page->theme_color; + $this->page->theme_path = dirname($this->page->theme_path) . '/' . $this->page->theme_color; + $this->page->layout_dir = $this->page->theme_dir . '/' . $this->page->theme_color; + $this->page->layout_style_type = \gp\tool\Output::StyleType($this->page->layout_dir); + } + + + /** + * Preview changes from the Customizer + * + */ + public function PreviewCustomizer(){ + $this->CustomizerResults(); + // msg('CustomizerResults() → $this->customizer_results = ' . pre($this->customizer_results)); + + if( !empty($this->customizer_results['php_vars']) ){ + $this->page->preview_layout_config = $this->customizer_results['php_vars']; + // msg('$this->page->preview_layout_config = ' . pre($this->page->preview_layout_config)); + } + + if( !empty($this->customizer_results['js_vars']) ){ + $this->page->head_script .= $this->customizer_results['js_vars']; + } + } + + + /** + * Process the post values from the customizer + * + */ + public function CustomizerResults(){ + + if( !isset($_POST['customizer']) ){ + $this->customizer_results = []; + return false; + } + + $this->SetLayoutValues(); + + $style_type = $this->page->layout_style_type; + $customizer_data = $this->GetLayoutCustomizer($this->curr_layout); + $messages = []; + + $customizer_vars = []; + foreach($customizer_data as $section => $section_data){ + foreach($section_data['items'] as $item_name => $item_data){ + $vars = []; + + $vars['default_value'] = $item_data['default_value']; + + if( !empty($item_data['default_units']) ){ + $vars['default_units'] = $item_data['default_units']; + } + + $vars['control_type'] = $item_data['control']['type']; + + if( isset($item_data['control']['min']) ){ + $vars['min'] = (float)$item_data['control']['min']; + } + + if( isset($item_data['control']['max']) ){ + $vars['max'] = (float)$item_data['control']['max']; + } + + if( !empty($item_data['control']['possible_values']) ){ + $vars['possible_values'] = array_values($item_data['control']['possible_values']); + } + + if( !empty($item_data['control']['units']) ){ + $vars['units'] = array_values($item_data['control']['units']); + } + + $vars['used_in'] = []; + if( !empty($item_data['control']['used_in']) ){ + $vars['used_in'] = $item_data['control']['used_in']; + } + + if( !empty($item_data['control']['pattern']) ){ + $vars['pattern'] = $item_data['control']['pattern']; + } + + $customizer_vars[$item_name] = $vars; + } + } + + $customizer_post_values =& $_POST['customizer']; + + $patterns = [ + 'color' => '/(#(?:[0-9a-f]{2}){2,4}$|(#[0-9a-f]{3}$)|' . + '(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\)$|' . + 'black$|silver$|gray$|whitesmoke$|maroon$|red$|purple$|' . + 'fuchsia$|green$|lime$|olivedrab$|yellow$|navy$|blue$|teal$|' . + 'aquamarine$|orange$|aliceblue$|antiquewhite$|aqua$|azure$|' . + 'beige$|bisque$|blanchedalmond$|blueviolet$|brown$|burlywood$|' . + 'cadetblue$|chartreuse$|chocolate$|coral$|cornflowerblue$|' . + 'cornsilk$|crimson$|currentcolor$|darkblue$|darkcyan$|darkgoldenrod$|' . + 'darkgray$|darkgreen$|darkgrey$|darkkhaki$|darkmagenta$|darkolivegreen$|' . + 'darkorange$|darkorchid$|darkred$|darksalmon$|darkseagreen$|darkslateblue$|' . + 'darkslategray$|darkslategrey$|darkturquoise$|darkviolet$|deeppink$|' . + 'deepskyblue$|dimgray$|dimgrey$|dodgerblue$|firebrick$|floralwhite$|' . + 'forestgreen$|gainsboro$|ghostwhite$|goldenrod$|gold$|greenyellow$|grey$|' . + 'honeydew$|hotpink$|indianred$|indigo$|ivory$|khaki$|lavenderblush$|lavender$|' . + 'lawngreen$|lemonchiffon$|lightblue$|lightcoral$|lightcyan$|lightgoldenrodyellow$|' . + 'lightgray$|lightgreen$|lightgrey$|lightpink$|lightsalmon$|lightseagreen$|' . + 'lightskyblue$|lightslategray$|lightslategrey$|lightsteelblue$|lightyellow$|' . + 'limegreen$|linen$|mediumaquamarine$|mediumblue$|mediumorchid$|mediumpurple$|' . + 'mediumseagreen$|mediumslateblue$|mediumspringgreen$|mediumturquoise$|' . + 'mediumvioletred$|midnightblue$|mintcream$|mistyrose$|moccasin$|navajowhite$|' . + 'oldlace$|olive$|orangered$|orchid$|palegoldenrod$|palegreen$|paleturquoise$|' . + 'palevioletred$|papayawhip$|peachpuff$|peru$|pink$|plum$|powderblue$|rosybrown$|' . + 'royalblue$|saddlebrown$|salmon$|sandybrown$|seagreen$|seashell$|sienna$|' . + 'skyblue$|slateblue$|slategray$|slategrey$|snow$|springgreen$|steelblue$|' . + 'tan$|thistle$|tomato$|transparent$|turquoise$|violet$|wheat$|white$|' . + 'yellowgreen$|rebeccapurple$)/i', + + 'url' => '%^(?:(?:https?://|ftp://|/|//))(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})' . + '(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})' . + '(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])' . + '(?:\.?(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.?(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))' . + '|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.' . + '(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.?' . + '(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$%iuS', + + 'onoff' => '/(on|off)/', + + 'number' => '/^[0-9]+(\.[0-9]+)?$/', + + 'integer' => '/^[0-9]+$/', + ]; + + $ctl_types = [ + // name use named regex pattern + 'text' => false, + 'select' => false, + 'radio' => false, + 'number' => 'number', + 'checkbox' => 'onoff', + 'colorpicker' => 'color', + 'colors' => 'color', + 'url' => 'url', + 'image' => 'url', + 'file' => 'url', + ]; + + $layout_config = []; + $php_vars = []; + $scssless_vars = ''; + $css_vars = ''; + $js_vars = []; + + foreach($customizer_post_values as $var_name => $values){ + if( !array_key_exists($var_name, $customizer_vars) ){ + $messages[] = 'Undefined variable name ' . htmlspecialchars($var_name); + continue; + } + + // validate value + $current_val = $values['value']; + $current_units = isset($values['units']) ? $values['units'] : ''; + + $ctl_type = $customizer_vars[$var_name]['control_type']; + if( !array_key_exists($ctl_type, $ctl_types) ){ + $messages[] = 'Unregistered control type ' . htmlspecialchars($ctl_type); + continue; + } + + // validate via regex + $pattern = false; + if( !empty($customizer_vars[$var_name]['pattern']) ){ + // pattern is defined in customizer.php + $pattern = $customizer_vars[$var_name]['pattern']; + }else{ + // use pattern defined for control type + $pattern = $ctl_types[$ctl_type]; + } + + if( !empty($currrent_val) && $pattern ){ + if( array_key_exists($pattern, $patterns) ){ + // using named regex pattern + $pattern = $patterns[$pattern]; + } + if( !preg_match($pattern, $current_val) ){ + $messages[] = 'Invalid value ' . htmlspecialchars($current_val) . + ' for variable ' . htmlspecialchars($var_name) . + ' (pattern does not match)'; + continue; + } + } + + // validate possible values + if( !empty($customizer_vars[$var_name]['possible_values']) ){ + if( $current_val && !in_array($current_val, $customizer_vars[$var_name]['possible_values']) ){ + $messages[] = 'Invalid value ' . htmlspecialchars($current_val) . + ' for variable ' . htmlspecialchars($var_name) . + ' (not in possible values)'; + continue; + } + } + + // validate possible units + if( !empty($customizer_vars[$var_name]['units']) ){ + if( !in_array($current_units, $customizer_vars[$var_name]['units']) ){ + $messages[] = 'Invalid units ' . htmlspecialchars($current_units) . + ' for variable ' . htmlspecialchars($var_name); + continue; + } + } + + // add to used envoronments + foreach($customizer_vars[$var_name]['used_in'] as $used_in){ + + $current_val_bool = $current_val; + if( $current_val == 'on' ){ + $current_val_bool = true; + } + if( $current_val == 'off' ){ + $current_val_bool = false; + } + + $layout_config[$var_name] = [ 'value' => $current_val_bool ]; + if( !empty($current_units) ){ + $layout_config[$var_name]['units'] = $current_units; + } + + $used_in = strtolower($used_in); + switch($used_in){ + case 'php': + $php_vars[$var_name] = $layout_config[$var_name]; + break; + + case 'js': + $js_vars[$var_name] = $layout_config[$var_name]; + break; + + case 'scssless': + $prefix = $style_type == 'scss' ? '$' : '@'; + if( !empty($current_val) ){ + $scssless_vars .= $prefix . $var_name . ': ' . $current_val . $current_units . ";\n"; + } + break; + + case 'css': + if( !empty($current_val) ){ + $css_vars .= ' --' . $var_name . ': ' . $current_val . $current_units . ";\n"; + } + break; + } + } + } + + if( !empty($css_vars) ){ + // css vars will use the :root scope which has a higher specificity than html + $css_vars = "\n" . ':root {' . "\n" . $css_vars . '}' . "\n"; + } + + $js_vars = "\n" . 'var layout_config = ' . json_encode($js_vars) . ';' . "\n"; + + if(!empty($messages) ){ + msg(implode('
    ' , $messages)); + } + + $customizer_results = [ + 'layout_config' => $layout_config, + 'php_vars' => $php_vars, + 'js_vars' => $js_vars, + 'scssless_vars' => $scssless_vars, + 'css_vars' => $css_vars, + ]; + + $this->customizer_results = $customizer_results; + } + + + /** + * Preview changes to the custom css/scss/less + * + */ + public function PreviewCSS(){ + global $langmessage; + + $this->SetLayoutValues(); + + $style_files = []; + + switch($this->page->layout_style_type){ + case 'scss': + $this->PreviewScss($this->page->layout_dir); + return; + + case 'less': + $this->PreviewLess($this->page->layout_dir); + return; + + case 'css': + default: + $this->page->css_user[] = rawurldecode($this->page->theme_path) . '/style.css'; + + $css = ''; + + // customizer css vars + if( !empty($this->customizer_results['css_vars']) ){ + $css .= $this->customizer_results['css_vars']; + } + + // custom css + $css .= $_REQUEST['css']; + + if( !empty(trim($css)) ){ + //make sure this is seen as code and not a filename + $style_files[] = $css . "\n"; + // let's use the Scss Compiler to validate the user css + $parsed_data = \gp\tool\Output\Css::ParseScss($style_files); + $compiled = $parsed_data[0]; + + if( $compiled === false ){ + msg($langmessage['OOPS'] . ' (Invalid CSS)'); + return false; + } + $this->page->head .= ''; + } + break; + } + + $this->page->get_theme_css = false; + } + + + /** + * Order of files for LESS + * variables.less + * custom.less (posted) + * customizer.scss (posted) + * Bootstrap.less + */ + protected function PreviewLess($dir){ + global $langmessage; + + $style_files = []; + + // variables.less + $var_file = $dir . '/variables.less'; + if( file_exists($var_file) ){ + $style_files[] = $var_file; + } + + // custom less + $temp_less = trim($_REQUEST['css']); + if( !empty($temp_less) ){ + //make sure this is seen as code and not a filename + $style_files[] = $_REQUEST['css'] . "\n"; + } + + // customizer less vars + if( !empty($this->customizer_results['scssless_vars']) ){ + //make sure this is seen as code and not a filename + $style_files[] = $this->customizer_results['scssless_vars'] . "\n"; + } + + // customizer css vars + if( !empty($this->customizer_results['css_vars']) ){ + //make sure this is seen as code and not a filename + $style_files[] = $this->customizer_results['css_vars'] . "\n"; + } + + // layout style file + $style_files[] = $dir . '/style.less'; + + $parsed_data = \gp\tool\Output\Css::ParseLess($style_files); + $compiled = $parsed_data[0]; + + if( $compiled === false ){ + msg($langmessage['OOPS'] . ' (Invalid LESS)'); + return false; + } + + $this->page->head .= ''; + $this->page->get_theme_css = false; + } + + + /** + * Order of files for SCSS + * variables.scss + * custom.scss (posted) + * customizer.scss (posted) + * Bootstrap.scss + */ + protected function PreviewScss($dir){ + global $langmessage; + + $style_files = []; + + // variables.scss + $var_file = $dir . '/variables.scss'; + if( file_exists($var_file) ){ + $style_files[] = $var_file; + } + + // custom scss + $temp_scss = trim($_REQUEST['css']); + if( !empty($temp_scss) ){ + //make sure this is seen as code and not a filename + $style_files[] = $_REQUEST['css'] . "\n"; + } + + // customizer scss vars + if( !empty($this->customizer_results['scssless_vars']) ){ + //make sure this is seen as code and not a filename + $style_files[] = $this->customizer_results['scssless_vars'] . "\n"; + } + + // customizer css vars + if( !empty($this->customizer_results['css_vars']) ){ + //make sure this is seen as code and not a filename + $style_files[] = $this->customizer_results['css_vars'] . "\n"; + } + + // layout style file + $style_files[] = $dir . '/style.scss'; + + $parsed_data = \gp\tool\Output\Css::ParseScss($style_files); + $compiled = $parsed_data[0]; + + if( $compiled === false ){ + msg($langmessage['OOPS'] . ' (Invalid SCSS)'); + return false; + } + + $this->page->head .= ''; + $this->page->get_theme_css = false; + } + + + public function DragArea(){ + global $langmessage; + + if( !$this->GetValues($_GET['dragging'], $from_container, $from_gpOutCmd) ){ + return; + } + if( !$this->GetValues($_GET['to'], $to_container, $to_gpOutCmd) ){ + return; + } + + + //prep work + $handlers = $this->GetAllHandlers(); + $this->PrepContainerHandlers($handlers, $from_container, $from_gpOutCmd); + $this->PrepContainerHandlers($handlers, $to_container, $to_gpOutCmd); + + + //remove from from_container + if( !isset($handlers[$from_container]) || !is_array($handlers[$from_container]) ){ + msg($langmessage['OOPS'] . ' (2)'); + return; + } + + $where = $this->ContainerWhere($from_gpOutCmd, $handlers[$from_container]); + $to = $this->ContainerWhere($to_gpOutCmd, $handlers[$from_container], false); + + if( $where === false ){ + return; + } + + array_splice($handlers[$from_container], $where, 1); + + /** + * for moving down + * if target is the same container + * and target is below dragged element + * then $offset = 1 + * + */ + $offset = 0; + if( ($from_container == $to_container) && + ($to !== false) && + ($to > $where) + ){ + $offset = 1; + } + + if( !$this->AddToContainer($handlers[$to_container], $to_gpOutCmd, $from_gpOutCmd, false, $offset) ){ + return; + } + + $this->SaveHandlersNew($handlers); + } + + + /** + * Display dialog for insterting gadgets/menus/etc into layouts + * + */ + public function SelectContent(){ + global $langmessage, $config; + + if( !isset($_GET['param']) ){ + msg($langmessage['OOPS'] . ' (Param not set)'); + return; + } + $param = $_GET['param']; + + //counts + $count_gadgets = + (isset($config['gadgets']) && is_array($config['gadgets'])) ? + count($config['gadgets']) : + false; + + echo '
    '; + + echo ''; // /.gp_tabs + + $this->SelectContent_Areas($param, $count_gadgets); + + echo '
    '; // /.inline_box + } + + + public function SelectContent_Areas($param, $count_gadgets){ + global $dataDir, $langmessage, $config; + + $addQuery = 'cmd=addcontent&where=' . rawurlencode($param); + echo '
    '; + + //extra content + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + + $extrasFolder = $dataDir . '/data/_extra'; + $files = scandir($extrasFolder) or []; + + asort($files); + foreach($files as $file){ + + $extraName = \gp\admin\Content\Extra::AreaExists($file); + if( $extraName === false ){ + continue; + } + + echo ''; + echo ''; + echo ''; + echo ''; + } + + //new extra area + echo ''; + echo ''; + echo ''; + echo '
     
    '; + echo str_replace('_', ' ', $extraName); + echo ''; + echo \gp\tool::Link( + $this->layout_slug, + $langmessage['add'], + $addQuery . '&insert=Extra:' . $extraName, + ['data-cmd' => 'creq'] + ); + echo '
    '; + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + $types = \gp\tool\Output\Sections::GetTypes(); + echo ' '; + + echo ''; + echo '
    '; + echo '
    '; + + echo '

    '; + echo '

    '; + echo ''; + echo '
    '; + echo '

    '; + echo '
    '; // /#layout_extra_content + + + //gadgets + if( $count_gadgets > 0 ){ + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach($config['gadgets'] as $gadget => $info){ + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + + echo '
     
    '; + echo str_replace('_', ' ', $gadget); + echo ''; + echo \gp\tool::Link( + $this->layout_slug, + $langmessage['add'], + $addQuery . '&insert=' . $gadget, + ['data-cmd'=>'creq'] + ); + echo '
    '; + echo ''; + echo '
    '; + echo '
    '; // /#layout_gadgets + } + + //menus + echo '
    '; + echo '
    '; + echo ''; + echo ''; + echo ''; + + echo ''; + $this->PresetMenuForm(); + + echo ''; + echo ''; + echo ''; + echo '
    '; + echo ''; + echo ''; + echo '
    '; + echo '
    '; + echo '
    '; // /#layout_menus + + //custom area + echo '
    '; + echo '
    '; + echo ''; + echo ''; + echo ''; + + echo ''; + $this->CustomMenuForm(); + + echo ''; + echo ''; + echo ''; + echo '
    '; + echo ''; + echo ' '; + echo '
    '; + echo '
    '; + echo '
    '; // /#layout_custom + + echo '
    '; + } + + + /** + * Insert new content into a layout + * + */ + public function AddContent(){ + global $langmessage; + + //for ajax responses + $this->page->ajaxReplace = []; + + if( !isset($_REQUEST['where']) ){ + msg($langmessage['OOPS']); + return false; + } + + //prep destination + if( !$this->GetValues($_REQUEST['where'], $to_container, $to_gpOutCmd) ){ + return false; + } + $handlers = $this->GetAllHandlers(); + $this->PrepContainerHandlers($handlers, $to_container, $to_gpOutCmd); + + //figure out what we're inserting + $addtype =& $_REQUEST['addtype']; + switch($addtype){ + case 'new_extra': + $extra_name = $this->NewExtraArea(); + if( $extra_name === false ){ + msg($langmessage['OOPS'] . ' (2)'); + return false; + } + $insert = 'Extra:' . $extra_name; + break; + + case 'custom_menu': + $insert = $this->NewCustomMenu(); + break; + + case 'preset_menu': + $insert = $this->NewPresetMenu(); + break; + + default: + $insert = $_REQUEST['insert']; + break; + } + + if( !$insert ){ + msg($langmessage['OOPS'] . ' (Nothing to insert)'); + return false; + } + + //new info + $new_gpOutInfo = \gp\tool\Output::GetgpOutInfo($insert); + if( !$new_gpOutInfo ){ + msg($langmessage['OOPS'] . ' (Nothing to insert)'); + return false; + } + $new_gpOutCmd = rtrim($new_gpOutInfo['key'] . ':' . $new_gpOutInfo['arg'], ':'); + + if( !$this->AddToContainer($handlers[$to_container], $to_gpOutCmd, $new_gpOutCmd, false) ){ + return false; + } + + $this->SaveHandlersNew($handlers); + + return true; + } + + + /** + * Return the name of the cleansed extra area name, create file if it doesn't already exist + * + */ + public function NewExtraArea(){ + global $langmessage, $dataDir; + + $title = \gp\tool\Editing::CleanTitle($_REQUEST['extra_area']); + if( empty($title) ){ + msg($langmessage['OOPS']); + return false; + } + + $data = \gp\tool\Editing::DefaultContent($_POST['type']); + $file = $dataDir . '/data/_extra/' . $title . '/page.php'; + + if( \gp\admin\Content\Extra::AreaExists($title) !== false ){ + return $title; + } + + if( !\gp\tool\Files::SaveData($file, 'file_sections', [$data]) ){ + msg($langmessage['OOPS']); + return false; + } + + return $title; + } + + + public function RemoveArea(){ + global $langmessage; + + //for ajax responses + $this->page->ajaxReplace = []; + + if( !$this->ParseHandlerInfo($_GET['param'], $curr_info) ){ + msg($langmessage['OOPS'] . ' (0)'); + return; + } + $gpOutCmd = $curr_info['gpOutCmd']; + $container = $curr_info['container']; + + //prep work + $handlers = $this->GetAllHandlers(); + $this->PrepContainerHandlers($handlers, $container, $gpOutCmd); + + //remove from $handlers[$container] + $where = $this->ContainerWhere($gpOutCmd, $handlers[$container]); + if( $where === false ){ + return; + } + + array_splice($handlers[$container], $where, 1); + + $this->SaveHandlersNew($handlers); + } + + + /** + * Get the position of $gpOutCmd in $container_info + * @return int|false + * + */ + public function ContainerWhere($gpOutCmd, &$container_info, $warn=true){ + global $langmessage; + + $where = array_search($gpOutCmd, $container_info); + + if( !is_int($where) ){ + if( $warn ){ + msg($langmessage['OOPS'] . ' (Not found in container)'); + } + return false; + } + + return $where; + } + + + /** + * Display popup dialog for editing layout menus + * + */ + public function LayoutMenu(){ + global $langmessage, $gpLayouts; + + if( !$this->ParseHandlerInfo($_GET['handle'], $curr_info) ){ + msg($langmessage['00PS']); + return; + } + + $showCustom = false; + $menu_args = $this->MenuArgs($curr_info); + + if( $curr_info['key'] == 'CustomMenu' ){ + $showCustom = true; + } + + echo '
    '; + + echo ''; // /.gp_tabs + + echo '
    '; + + echo '
    '; + + //preset menus + $style = $showCustom ? ' class="nodisplay"' : ''; + echo '
    '; + echo '
    '; + echo ''; + + echo ''; + $this->PresetMenuForm($menu_args); + + echo ''; + echo ''; + echo ''; + echo '
    '; + echo ' '; + echo ''; + echo '
    '; + echo '
    '; + echo '
    '; // /#layout_menus + + //custom menus + $style = $showCustom ? '' : ' class="nodisplay"'; + echo '
    '; + echo '
    '; + echo ''; + + echo ''; + $this->CustomMenuForm($menu_args); + + echo ''; + echo ''; + echo ''; + echo '
    '; + echo ''; + echo ' '; + echo '
    '; + echo '
    '; + echo '
    '; // /#layout_custom + + echo '

    '; + echo $langmessage['see_also']; + echo ' '; + echo \gp\tool::Link( + 'Admin/Menu', + $langmessage['file_manager'] + ); + echo ', '; + echo \gp\tool::Link( + 'Admin_Theme_Content', + $langmessage['content_arrangement'] + ); + echo '

    '; + + echo '
    '; // /#area_lists + + echo '
    '; // /.inline_box + } + + + /** + * Save the posted layout menu settings + * + */ + public function LayoutMenuSave(){ + global $langmessage, $gpLayouts; + + if( !$this->ParseHandlerInfo($_POST['handle'], $curr_info) ){ + msg($langmessage['OOPS'] . ' (0)'); + return; + } + + if( isset($_POST['new_handle']) ){ + $new_gpOutCmd = $this->NewPresetMenu(); + }else{ + $new_gpOutCmd = $this->NewCustomMenu(); + } + + if( $new_gpOutCmd === false ){ + msg($langmessage['OOPS'] . ' (1)'); + return; + } + + //prep + $handlers = $this->GetAllHandlers($this->curr_layout); + $container =& $curr_info['container']; + $this->PrepContainerHandlers($handlers, $container, $curr_info['gpOutCmd']); + + //unchanged? + if( $curr_info['gpOutCmd'] == $new_gpOutCmd ){ + return; + } + + if( !$this->AddToContainer($handlers[$container], $curr_info['gpOutCmd'], $new_gpOutCmd, true) ){ + return; + } + + $this->SaveHandlersNew($handlers, $this->curr_layout); + } + + + public function ParseHandlerInfo($str, &$info){ + global $config, $gpOutConf; + + if( substr_count($str, '|') !== 1 ){ + return false; + } + + list($container, $fullKey) = explode('|', $str); + + $arg = ''; + $pos = strpos($fullKey, ':'); + $key = $fullKey; + if( $pos > 0 ){ + $arg = substr($fullKey, $pos + 1); + $key = substr($fullKey, 0, $pos); + } + + if( !isset($gpOutConf[$key]) && !isset($config['gadgets'][$key]) ){ + return false; + } + + $info = []; + $info['gpOutCmd'] = trim($fullKey, ':'); + $info['container'] = $container; + $info['key'] = $key; + $info['arg'] = $arg; + + return true; + } + + + /** + * Get the container and gpOutCmd from the $arg + * + */ + public function GetValues($arg, &$container, &$gpOutCmd){ + global $langmessage; + + if( substr_count($arg, '|') !== 1 ){ + msg($langmessage['OOPS'] . ' (Invalid argument)'); + return false; + } + + list($container, $gpOutCmd) = explode('|', $arg); + return true; + } + + + public function AddToContainer(&$container_info, $to_gpOutCmd, $new_gpOutCmd, $replace=true, $offset=0){ + global $langmessage; + + //add to to_container in front of $to_gpOutCmd + if( !is_array($container_info) ){ + msg($langmessage['OOPS'] . ' (a1)'); + return false; + } + + + //can't have two identical outputs in the same container + $check = $this->ContainerWhere($new_gpOutCmd, $container_info, false); + if( $check !== false ){ + msg($langmessage['OOPS'] . ' (Area already in container)'); + return false; + } + + //if empty, just add + if( count($container_info) === 0 ){ + $container_info[] = $new_gpOutCmd; + return true; + } + + //insert + $where = $this->ContainerWhere($to_gpOutCmd, $container_info); + if( $where === false ){ + return false; + } + + $length = 1; + if( $replace === false ){ + $length = 0; + $where += $offset; + } + + array_splice($container_info, $where, $length, $new_gpOutCmd); + + return true; + } + + + public function NewCustomMenu(){ + + $upper_bound =& $_POST['upper_bound']; + $lower_bound =& $_POST['lower_bound']; + $expand_bound =& $_POST['expand_bound']; + $expand_all =& $_POST['expand_all']; + $source_menu =& $_POST['source_menu']; + + $this->CleanBounds($upper_bound, $lower_bound, $expand_bound, $expand_all, $source_menu); + + $arg = $upper_bound . ',' . + $lower_bound . ',' . + $expand_bound . ',' . + $expand_all . ',' . + $source_menu; + + return 'CustomMenu:' . $arg; + } + + + public function NewPresetMenu(){ + global $gpOutConf; + + $new_gpOutCmd =& $_POST['new_handle']; + if( !isset($gpOutConf[$new_gpOutCmd]) || !isset($gpOutConf[$new_gpOutCmd]['link']) ){ + return false; + } + + return rtrim($new_gpOutCmd . ':' . $this->CleanMenu($_POST['source_menu']), ':'); + } + + + public function PresetMenuForm($args=[]){ + global $gpOutConf, $langmessage; + + $current_function =& $args['current_function']; + $current_menu =& $args['source_menu']; + + $this->MenuSelect($current_menu); + + echo ''; + echo ''; + echo $langmessage['Menu Output']; + echo ''; + echo ''; + + $i = 0; + foreach($gpOutConf as $outKey => $info){ + + if( !isset($info['link']) ){ + continue; + } + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if( $current_function == $outKey ){ + echo ''; + }else{ + echo ''; + } + echo ''; + echo ''; + + $i++; + } + } + + + public function MenuArgs($curr_info){ + + $menu_args = []; + + if( $curr_info['key'] == 'CustomMenu' ){ + + $args = explode(',', $curr_info['arg']); + $args += [0 => 0, 1 => -1, 2 => -1, 3 => 0, 4 => '']; //defaults + list($upper_bound, $lower_bound, $expand_bound, $expand_all, $source_menu) = $args; + + $this->CleanBounds($upper_bound, $lower_bound, $expand_bound, $expand_all, $source_menu); + + $menu_args['upper_bound'] = $upper_bound; + $menu_args['lower_bound'] = $lower_bound; + $menu_args['expand_bound'] = $expand_bound; + $menu_args['expand_all'] = $expand_all; + $menu_args['source_menu'] = $source_menu; + + }else{ + + $menu_args['current_function'] = $curr_info['key']; + $menu_args['source_menu'] = $this->CleanMenu($curr_info['arg']); + + } + + return $menu_args; + } + + + /** + * Output form elements for setting custom menu settings + * @param array $menu_args + * + */ + public function CustomMenuForm($menu_args=[]){ + global $langmessage; + + $upper_bound =& $menu_args['upper_bound']; + $lower_bound =& $menu_args['lower_bound']; + $expand_bound =& $menu_args['expand_bound']; + $expand_all =& $menu_args['expand_all']; + $source_menu =& $menu_args['source_menu']; + + $this->MenuSelect($source_menu); + + echo ''; + echo ''; + echo $langmessage['Show Titles...']; + echo ''; + echo ''; + + $this->CustomMenuSection($langmessage['... Below Level'], 'upper_bound', $upper_bound); + $this->CustomMenuSection($langmessage['... At And Above Level'], 'lower_bound', $lower_bound); + + echo ''; + echo ''; + echo $langmessage['Expand Menu...']; + echo ''; + echo ''; + + $this->CustomMenuSection($langmessage['... Below Level'], 'expand_bound', $expand_bound); + + echo ''; + echo ''; + echo $langmessage['... Expand All']; + echo ''; + echo ''; + $attr = $expand_all ? ' checked' : ''; + echo ''; + echo ''; + echo ''; + } + + + public function CleanBounds(&$upper_bound, &$lower_bound, &$expand_bound, &$expand_all, &$source_menu){ + + $upper_bound = (int)$upper_bound; + $upper_bound = max(0, $upper_bound); + $upper_bound = min(4, $upper_bound); + + $lower_bound = (int)$lower_bound; + $lower_bound = max(-1, $lower_bound); + $lower_bound = min(4, $lower_bound); + + $expand_bound = (int)$expand_bound; + $expand_bound = max(-1, $expand_bound); + $expand_bound = min(4, $expand_bound); + + if( $expand_all ){ + $expand_all = 1; + }else{ + $expand_all = 0; + } + + $source_menu = $this->CleanMenu($source_menu); + } + + + public function CleanMenu($menu){ + global $config; + + if( empty($menu) ){ + return ''; + } + + if( !isset($config['menus'][$menu]) ){ + return ''; + } + + return $menu; + } + + + /** + * Output section for custom menu form + * @param string $label + * @param string $name + * @param int $value + * + */ + public function CustomMenuSection($label, $name, $value){ + echo ''; + echo ''; + echo $label; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + + public function MenuSelect($source_menu){ + global $config, $langmessage; + + echo ''; + echo ''; + echo $langmessage['Source Menu']; + echo ''; + echo ''; + echo ''; + echo ''; + echo $langmessage['Menu']; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + +} diff --git a/include/admin/Layout/Image.php b/include/admin/Layout/Image.php new file mode 100644 index 0000000..bcb4f74 --- /dev/null +++ b/include/admin/Layout/Image.php @@ -0,0 +1,197 @@ +cmds['InlineEdit'] = ''; //added to in js + $this->cmds['gallery_folder'] = 'GalleryImages'; + $this->cmds['gallery_images'] = 'GalleryImages'; + $this->cmds['save_inline'] = 'SaveHeaderImage'; + $this->cmds['image_editor'] = '\\gp\\tool\\Editing::ImageEditor'; + } + + + /** + * Load the inline editor for a theme image + * + */ + public function InlineEdit(){ + + $section = array(); + $section['type'] = 'image'; + \gp\tool\Output\Ajax::InlineEdit($section); + die(); + } + + public function GalleryImages(){ + $_GET += array('dir'=>'/headers'); + \gp\admin\Content\Uploaded::InlineList($_GET['dir']); + } + + + + /** + * Save a theme image + * Resize image if necessary + * + */ + public function SaveHeaderImage(){ + global $gpLayouts,$langmessage; + + $this->page->ajaxReplace = array(); + + + $section = array(); + if( !\gp\tool\Editing::SectionFromPost_Image($section, '/data/_uploaded/headers/') ){ + return false; + } + + + $save_info = array(); + $save_info['img_rel'] = $section['attributes']['src']; + $save_info['width'] = $section['attributes']['width']; + $save_info['height'] = $section['attributes']['height']; + + $container = $_REQUEST['container']; + $gpLayouts[$this->curr_layout]['images'][$container] = array(); //prevents shuffle + $gpLayouts[$this->curr_layout]['images'][$container][] = $save_info; + + + if( !$this->SaveLayouts() ){ + return false; + } + $this->page->ajaxReplace[] = array('ck_saved','',''); + return true; + } + + + /** + * Show images available in themes + * + */ + public function ShowThemeImages(){ + global $langmessage; + + $this->page->ajaxReplace = array(); + $current_theme = false; + + //which theme folder + if( isset($_REQUEST['theme']) && isset($this->avail_addons[$_REQUEST['theme']]) ){ + $current_theme = $_REQUEST['theme']; + $current_info = $this->avail_addons[$current_theme]; + $current_label = $current_info['name']; + $current_dir = $current_info['full_dir']; + $current_url = \gp\tool::GetDir($current_info['rel']); + + //current layout + }else{ + $layout_info = \gp\tool::LayoutInfo($this->curr_layout,false); + $current_label = $layout_info['theme_name']; + $current_dir = $layout_info['dir']; + $current_url = \gp\tool::GetDir(dirname($layout_info['path'])); + } + + + //list of themes + ob_start(); + echo '
    '; + echo ''; + echo $current_label; + echo ''; + + echo '
    '; + + foreach($this->avail_addons as $theme_id => $info){ + $slug = 'Admin_Theme_Content/Image/'.rawurlencode($this->curr_layout); + echo \gp\tool::Link($slug,''.$info['name'],'cmd=ShowThemeImages&theme='.rawurlencode($theme_id),' data-cmd="gpajax" class="gp_gallery_folder" '); + } + echo '
    '; + echo '
    '; + + $gp_option_area = ob_get_clean(); + + + //images in theme + $images = array(); + self::GetAvailThemeImages( $current_dir, $current_url, $images ); + ob_start(); + foreach($images as $image ){ + echo ''; + } + $gp_gallery_avail_imgs = ob_get_clean(); + + + if( $current_theme ){ + $this->page->ajaxReplace[] = array('inner','#gp_option_area',$gp_option_area); + $this->page->ajaxReplace[] = array('inner','#gp_gallery_avail_imgs',$gp_gallery_avail_imgs); + }else{ + $content = '
    '.$gp_option_area.'
    ' + .''; + $this->page->ajaxReplace[] = array('inner','#gp_image_area',$content); + } + + $this->page->ajaxReplace[] = array('inner','#gp_folder_options',''); //remove upload button + } + + + /** + * Get a list of all the images available within a theme + * + */ + public function GetAvailThemeImages( $dir, $url, &$images ){ + + $files = scandir($dir); + $files = array_diff($files,array('.','..')); + foreach($files as $file){ + + $file_full = $dir.'/'.$file; + $file_url = $url.'/'.$file; + if( is_dir($file_full) ){ + self::GetAvailThemeImages( $file_full, $file_url, $images ); + continue; + } + + if( !self::IsImg( $file ) ){ + continue; + } + + $size = getimagesize($file_full); + + $temp = array(); + $temp['width'] = $size[0]; + $temp['height'] = $size[1]; + $temp['full'] = $file_full; + $temp['url'] = $file_url; + $images[] = $temp; + } + } + + + /** + * Determines if the $file is an image based on the file extension + * @static + * @return bool + */ + public static function IsImg($file){ + $img_types = array('bmp','png','jpg','jpeg','gif','tiff','tif'); + + $name_parts = explode('.',$file); + $file_type = array_pop($name_parts); + $file_type = strtolower($file_type); + + return in_array($file_type,$img_types); + } + + +} \ No newline at end of file diff --git a/include/admin/Layout/Remote.php b/include/admin/Layout/Remote.php new file mode 100644 index 0000000..c8c9b39 --- /dev/null +++ b/include/admin/Layout/Remote.php @@ -0,0 +1,15 @@ +cmds['EditText'] = ''; + $this->cmds['SaveText'] = 'ReturnHeader'; + + $this->cmds['AddonTextForm'] = ''; + $this->cmds['SaveAddonText'] = 'ReturnHeader'; + + $cmd = \gp\tool::GetCommand(); + $this->RunCommands($cmd); + } + + + + public function AddonTextForm(){ + global $langmessage,$config; + + $addon = \gp\tool\Editing::CleanArg($_REQUEST['addon']); + $texts = $this->GetAddonTexts($addon); + + //not set up correctly + if( $texts === false ){ + $this->EditText(); + return; + } + + + echo '
    '; + echo '
    '; + echo ''; + echo ''; //will be populated by javascript + + + $this->AddonTextFields($texts); + echo ' '; + echo ' '; + + + echo '
    '; + echo '
    '; + + } + + public function AddonTextFields($array){ + global $langmessage,$config; + echo ''; + echo ''; + + $key =& $_GET['key']; + foreach($array as $text){ + + $value = $text; + if( isset($langmessage[$text]) ){ + $value = $langmessage[$text]; + } + if( isset($config['customlang'][$text]) ){ + $value = $config['customlang'][$text]; + } + + $style = ''; + if( $text == $key ){ + $style = ' style="background-color:#f5f5f5"'; + } + + echo ''; + + } + echo '
    '; + echo $langmessage['default']; + echo ''; + echo '
    '; + echo $text; + echo ''; + echo ''; //value has already been escaped with htmlspecialchars() + echo '
    '; + } + + + public function EditText(){ + global $config, $langmessage; + + if( !isset($_GET['key']) ){ + msg($langmessage['OOPS'].' (0)'); + return; + } + + $default = $value = $key = $_GET['key']; + if( isset($langmessage[$key]) ){ + $default = $value = $langmessage[$key]; + }else{ + $default = $value = htmlspecialchars($key); + } + if( isset($config['customlang'][$key]) ){ + $value = $config['customlang'][$key]; + }else{ + $value = htmlspecialchars($key); + } + + + + echo '
    '; + echo '
    '; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo '
    '; + echo $langmessage['default']; + echo ''; + echo $langmessage['edit']; + echo '
    '; + echo $default; + echo ''; + //$value is already escaped using htmlspecialchars() + echo ''; + echo '
    '; + echo '

    '; + echo ' '; + echo ' '; + echo '

    '; + + echo '
    '; + echo '
    '; + } + + + + public function SaveText(){ + global $config, $langmessage; + + if( !isset($_POST['key']) ){ + msg($langmessage['OOPS'].' (0)'); + return; + } + if( !isset($_POST['value']) ){ + msg($langmessage['OOPS'].' (1)'); + return; + } + + $default = $key = $_POST['key']; + if( isset($langmessage[$key]) ){ + $default = $langmessage[$key]; + } + + $config['customlang'][$key] = $value = htmlspecialchars($_POST['value']); + if( ($value === $default) || (htmlspecialchars($default) == $value) ){ + unset($config['customlang'][$key]); + } + + + $this->SaveConfig(); + } + + + public function SaveAddonText(){ + global $langmessage,$config; + + $addon = \gp\tool\Editing::CleanArg($_REQUEST['addon']); + $texts = $this->GetAddonTexts($addon); + //not set up correctly + if( $texts === false ){ + msg($langmessage['OOPS'].' (0)'); + return; + } + + foreach($texts as $text){ + if( !isset($_POST['values'][$text]) ){ + continue; + } + + + $default = $text; + if( isset($langmessage[$text]) ){ + $default = $langmessage[$text]; + } + + $value = htmlspecialchars($_POST['values'][$text]); + + if( ($value === $default) || (htmlspecialchars($default) == $value) ){ + unset($config['customlang'][$text]); + }else{ + $config['customlang'][$text] = $value; + } + } + + + if( $this->SaveConfig() ){ + $this->UpdateAddon($addon); + } + + } + + + + public function UpdateAddon($addon){ + if( !function_exists('OnTextChange') ){ + return; + } + + \gp\tool\Plugins::SetDataFolder($addon); + + OnTextChange(); + + \gp\tool\Plugins::ClearDataFolder(); + } + + public function GetAddonTexts($addon){ + global $langmessage,$config; + + + $addon_config = \gp\tool\Plugins::GetAddonConfig($addon); + $addonDir = $addon_config['code_folder_full']; + if( !is_dir($addonDir) ){ + return false; + } + + //not set up correctly + if( !isset($config['addons'][$addon]['editable_text']) ){ + return false; + } + + $file = $addonDir.'/'.$config['addons'][$addon]['editable_text']; + if( !file_exists($file) ){ + return false; + } + + $texts = array(); + include($file); + + if( empty($texts) ){ + return false; + } + + return $texts; + } + +} diff --git a/include/admin/Login.php b/include/admin/Login.php new file mode 100644 index 0000000..e6d5fef --- /dev/null +++ b/include/admin/Login.php @@ -0,0 +1,266 @@ +requested = str_replace(' ','_',$title); + $this->title = $title; + $this->lang = $config['language']; + $this->language = $languages[$this->lang]; + $this->get_theme_css = false; + $_REQUEST['gpreq'] = 'admin'; + + $this->head .= "\n".''; + @header( 'X-Frame-Options: SAMEORIGIN' ); + } + + public function RunScript(){} + + public function GetGpxContent(){ + + $this->head_js[] = '/include/js/login.js'; + $this->head_js[] = '/include/js/md5_sha.js'; + $this->head_js[] = '/include/thirdparty/js/jsSHA.js'; + + $this->css_admin[] = '/include/css/login.css'; + $_POST += array('username'=>''); + + $this->admin_js = true; + \gp\tool\Session::HasCookies(); + + + $this->BrowserWarning(); + $this->JavascriptWarning(); + + echo '
    '; + echo '
    '; + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'send_password'; + if( $this->SendPassword() ){ + $this->LoginForm(); + }else{ + $this->FogottenPassword(); + } + break; + + case 'forgotten': + $this->FogottenPassword(); + break; + default: + $this->LoginForm(); + break; + } + + echo '
    '; + echo '
    '; + } + + + public function FogottenPassword(){ + global $langmessage; + + $_POST += array('username'=>''); + $this->css_admin[] = '/include/css/login.css'; + + + echo '
    '; + echo '
    '; + + echo ''; + + echo ''; + echo ''; + echo '   '; + + echo '
    '; + echo '
    '; + + } + + public function LoginForm(){ + global $langmessage; + + + $_REQUEST += array('file'=>''); + + + echo '
    '; + echo '
    Log in Timeout: '.\gp\tool::Link('Admin','Reload to continue...').'
    '; + + echo '
    '; + echo ''; //for redirection + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo '
    '; + + echo ''; + + echo ''; + + echo '

    '; + echo ''; + echo '   '; + echo \gp\tool::Link('',$langmessage['cancel']); + echo '

    '; + + echo '

    '; + echo ' '; + + echo ''; + echo '

    '; + + echo '
    '; + echo ''; + echo '
    '; + + + echo '
    '; + echo '
    '; + } + + public function BrowserWarning(){ + global $langmessage; + + echo '
    '; + echo '

    '.$langmessage['Browser Warning'].'

    '; + echo '

    '.$langmessage['Browser !Supported'].'

    '; + echo ' Mozilla Firefox'; + echo ' Google Chrome'; + echo ' Opera'; + echo ' Apple Safari'; + echo ' Microsoft Edge'; + echo '
    '; + } + + public function JavascriptWarning(){ + global $langmessage; + + echo '
    '; + echo '

    '.$langmessage['JAVASCRIPT_REQ'].'

    '; + echo '

    '; + echo $langmessage['INCOMPAT_BROWSER']; + echo ' '; + echo $langmessage['MODERN_BROWSER']; + echo '

    '; + echo '
    '; + } + + + public function Checked($name){ + + if( strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST' ) + return ' checked="checked" '; + + if( !isset($_POST[$name]) ) + return ''; + + return ' checked="checked" '; + } + + + public function SendPassword(){ + global $langmessage, $config; + + $users = \gp\tool\Files::Get('_site/users'); + $username = $_POST['username']; + + if( !isset($users[$username]) ){ + msg($langmessage['OOPS']); + return false; + } + + $userinfo = $users[$username]; + + + + if( empty($userinfo['email']) ){ + msg($langmessage['no_email_provided']); + return false; + } + + $passwordChars = str_repeat('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 3); + $newpass = str_shuffle($passwordChars); + $newpass = substr($newpass, 0, 8); + $pass_hash = \gp\tool\Session::PassAlgo($userinfo); + $former_pass_hash = false; + + if( $pass_hash == 'password_hash' && !function_exists('password_hash') ){ + // this unlikely but possible case may only occur if a former PHP 5.5+ site was moved to a PHP < 5.5 host + // the password algorithm will then be changed to sha512. the old password will not be usable anymore + $former_pass_hash = $pass_hash; + $pass_hash = 'sha512'; + $users[$username]['passhash'] = $pass_hash; + } + + $users[$username]['newpass'] = \gp\tool::hash($newpass, $pass_hash); + if( !\gp\tool\Files::SaveData('_site/users', 'users', $users) ){ + msg($langmessage['OOPS'] . ' (User data not saved. Check file permissions)'); + return false; + } + + $server = \gp\tool::ServerName(); + $link = \gp\tool::AbsoluteLink('Admin', $langmessage['login']); + $message = sprintf($langmessage['passwordremindertext'], $server, $link, $username, $newpass); + + + // send email + $mailer = new \gp\tool\Emailer(); + + if( $mailer->SendEmail($userinfo['email'], $langmessage['new_password'], $message) ){ + list($namepart, $sitepart) = explode('@', $userinfo['email']); + $showemail = substr($namepart, 0, 3) . '...@' . $sitepart; + msg(sprintf($langmessage['password_sent'], $username, $showemail)); + return true; + } + + // sending the new password failed + + msg($langmessage['OOPS'].' (Email not sent)'); + + if( $former_pass_hash ){ + // although this will only help in the *very special* case, where the + // PHP version < 5.5 was changed to 5.5+ AFTER the (now failed) new password request + // we will restore the former password hash algorithm, so the old password (if recalled) will work again + $users[$username]['passhash'] = $former_pass_hash; + \gp\tool\Files::SaveData('_site/users', 'users', $users); + } + + return false; + } + + +} diff --git a/include/admin/Menu.php b/include/admin/Menu.php new file mode 100644 index 0000000..73b9952 --- /dev/null +++ b/include/admin/Menu.php @@ -0,0 +1,1863 @@ + true, + 'all' => true, + 'hidden' => true, + 'nomenus' => true + ); + + public $section_types; + + protected $cmd; + + + public function __construct($args){ + global $langmessage, $config; + + parent::__construct($args); + + $this->section_types = \gp\tool\Output\Sections::GetTypes(); + + $this->page->ajaxReplace = array(); + + $this->page->css_admin[] = '/include/css/admin_menu_new.css'; + + $this->page->head_js[] = '/include/thirdparty/js/nestedSortable.js'; + $this->page->head_js[] = '/include/thirdparty/js/jquery_cookie.js'; + $this->page->head_js[] = '/include/js/admin_menu_new.js'; + + $this->max_level_index = max(3, gp_max_menu_level-1); + $this->page->head_script .= 'var max_level_index = ' . $this->max_level_index . ';'; + + + $this->avail_menus['gpmenu'] = $langmessage['Main Menu'] . ' / ' . $langmessage['site_map']; + $this->avail_menus['all'] = $langmessage['All Pages']; + $this->avail_menus['hidden'] = $langmessage['Not In Main Menu']; + $this->avail_menus['nomenus'] = $langmessage['Not In Any Menus']; + $this->avail_menus['search'] = $langmessage['search pages']; + + if( isset($config['menus']) ){ + foreach($config['menus'] as $id => $menu_label){ + $this->avail_menus[$id] = $menu_label; + } + } + + //read cookie settings + if( isset($_COOKIE['gp_menu_prefs']) ){ + parse_str($_COOKIE['gp_menu_prefs'], $this->cookie_settings); + } + + $this->SetMenuID(); + $this->SetMenuArray(); + $this->SetCollapseSettings(); + $this->SetQueryInfo(); + + $cmd = \gp\tool::GetCommand(); + $this->cmd = \gp\tool\Plugins::Filter('MenuCommand', array($cmd)); + + } + + + + public function RunScript(){ + + if( $this->cmd === 'return' ){ + return; + } + + switch($this->cmd){ + + case 'drag': + $this->SaveDrag(); + break; + + + //layout + case 'layout': + case 'uselayout': + case 'restorelayout': + $page_layout = new \gp\Page\Layout($this->cmd, 'Admin/Menu', $this->query_string); + if( $page_layout->result() ){ + return; + } + break; + } + + $this->ShowForm(); + } + + + + /** + * @param string $href + * @param string $label + * @param string $query + * @param string|array $attr + * @param mixed $nonce_action + * + */ + public function Link($href, $label, $query='', $attr='', $nonce_action=false){ + $query = $this->MenuQuery($query); + return \gp\tool::Link($href, $label, $query, $attr, $nonce_action); + } + + public function GetUrl($href, $query='', $ampersands=true){ + $query = $this->MenuQuery($query); + return \gp\tool::GetUrl($href,$query,$ampersands); + } + + + + public function MenuQuery($query=''){ + if( !empty($query) ){ + $query .= '&'; + } + $query .= 'menu=' . $this->curr_menu_id; + if( strpos($query, 'page=') !== false ){ + //do nothing + }elseif( $this->search_page > 0 ){ + $query .= '&page=' . $this->search_page; + } + + //for searches + if( !empty($_REQUEST['q']) ){ + $query .= '&q='.urlencode($_REQUEST['q']); + } + + return $query; + } + + + public function SetQueryInfo(){ + + //search page + if( isset($_REQUEST['page']) && is_numeric($_REQUEST['page']) ){ + $this->search_page = (int)$_REQUEST['page']; + } + + //browse query string + $this->query_string = $this->MenuQuery(); + } + + + public function SetCollapseSettings(){ + $gp_menu_collapse =& $_COOKIE['gp_menu_hide']; + + $search = '#' . $this->curr_menu_id . '=['; + $pos = strpos($gp_menu_collapse, $search); + if( $pos === false ){ + return; + } + + $gp_menu_collapse = substr($gp_menu_collapse, $pos + strlen($search)); + $pos = strpos($gp_menu_collapse, ']'); + if( $pos === false ){ + return; + } + $gp_menu_collapse = substr($gp_menu_collapse, 0, $pos); + $gp_menu_collapse = trim($gp_menu_collapse, ','); + $this->hidden_levels = explode(',', $gp_menu_collapse); + $this->hidden_levels = array_flip($this->hidden_levels); + } + + + + /** + * Get the id for the current menu + * Not the same order as used for $_REQUEST + * + */ + public function SetMenuID(){ + + if( isset($this->curr_menu_id) ){ + return; + } + + if( isset($_POST['menu']) ){ + $this->curr_menu_id = $_POST['menu']; + }elseif( isset($_GET['menu']) ){ + $this->curr_menu_id = $_GET['menu']; + }elseif( isset($this->cookie_settings['gp_menu_select']) ){ + $this->curr_menu_id = $this->cookie_settings['gp_menu_select']; + } + + if( !isset($this->curr_menu_id) || !isset($this->avail_menus[$this->curr_menu_id]) ){ + $this->curr_menu_id = 'gpmenu'; + } + } + + + public function SetMenuArray(){ + global $gp_menu; + + if( isset($this->list_displays[$this->curr_menu_id]) ){ + return; + } + + //set curr_menu_array + if( $this->curr_menu_id == 'gpmenu' ){ + $this->curr_menu_array =& $gp_menu; + $this->is_main_menu = true; + return; + } + + $this->curr_menu_array = \gp\tool\Output\Menu::GetMenuArray($this->curr_menu_id); + $this->is_alt_menu = true; + } + + + public function SaveMenu($menu_and_pages=false){ + global $dataDir; + + if( $this->is_main_menu ){ + return \gp\admin\Tools::SavePagesPHP(); + } + + if( is_null($this->curr_menu_array) ){ + return false; + } + + if( $menu_and_pages && !\gp\admin\Tools::SavePagesPHP() ){ + return false; + } + + $menu_file = $dataDir . '/data/_menus/' . $this->curr_menu_id . '.php'; + return \gp\tool\Files::SaveData($menu_file, 'menu', $this->curr_menu_array); + } + + + + /** + * Primary Display + * + * + */ + public function ShowForm(){ + global $langmessage, $config; + + $menu_output = false; + ob_start(); + + if( isset($this->list_displays[$this->curr_menu_id]) ){ + $this->SearchDisplay(); + $replace_id = '#gp_menu_available'; + }else{ + $menu_output = true; + $this->OutputMenu(); + $replace_id = '#admin_menu'; + } + + $content = ob_get_clean(); + + // json response + if( isset($_REQUEST['gpreq']) && ($_REQUEST['gpreq'] == 'json') ){ + $this->MenuJsonResponse($replace_id, $content); + return; + } + + // search form + echo ''; + + $menus = $this->GetAvailMenus('menu'); + $lists = $this->GetAvailMenus('display'); + + //heading + echo '
    '; + echo ''; + + echo '

    '; + echo $langmessage['file_manager'] . ' » '; + echo ''; + echo '

    '; + echo '
    '; + + //homepage + echo '
    '; + $this->HomepageDisplay(); + echo '
    '; + \gp\tool\Editing::PrepAutoComplete(); + + echo '
    '; + + if( $menu_output ){ + echo '
      '; + echo $content; + echo '
    '; + + echo ''; + + echo ''; + + echo ''; + }else{ + echo '
    '; + echo $content; + echo '
    '; + } + + echo '
    '; + + echo '
    '; + + echo '
    '; + echo ''.$langmessage['Menus'].''; + $this->MenuList($menus); + echo ''; + echo \gp\tool::Link( + 'Admin/Menu/Menus', + '+ ' . $langmessage['Add New Menu'], + 'cmd=NewMenuPrompt', + 'data-cmd="gpabox"' + ); + echo ''; + echo '
    '; + + echo '
    '; + echo ''.$langmessage['Lists'].''; + $this->MenuList($lists); + echo '
    '; + + //options for alternate menu + if( $this->is_alt_menu ){ + echo '
    '; + $label = $menus[$this->curr_menu_id]; + echo ''.$label.''; + echo ''; + echo \gp\tool::Link( + 'Admin/Menu/Menus', + $langmessage['rename'], + 'cmd=MenuRenamePrompt&id=' . $this->curr_menu_id, + 'data-cmd="gpabox"' + ); + echo ''; + $title_attr = sprintf($langmessage['generic_delete_confirm'], '"' . $label . '"'); + echo ''; + echo \gp\tool::Link( + 'Admin/Menu/Menus', + $langmessage['delete'], + 'cmd=MenuRemove&id=' . $this->curr_menu_id, + array( + 'data-cmd' => 'cnreq', + 'class' => 'gpconfirm', + 'title' => $title_attr + ) + ); + echo ''; + + echo '
    '; + } + + echo '
    '; + + echo '
    '; + } + + + + /** + * Generate link list for available menus + * + */ + public function MenuList($menus){ + foreach($menus as $menu_id => $menu_label){ + if( $menu_id == $this->curr_menu_id ){ + echo '' . $menu_label . ''; + }else{ + echo ''; + echo \gp\tool::Link( + 'Admin/Menu', + $menu_label, + 'menu=' . $menu_id, + array('data-cmd' => 'cnreq') + ); + echo ''; + } + } + } + + + public function GetAvailMenus($get_type='menu'){ + + $result = array(); + foreach($this->avail_menus as $menu_id => $menu_label){ + + $menu_type = 'menu'; + if( isset($this->list_displays[$menu_id]) ){ + $menu_type = 'display'; + } + + if( $menu_type == $get_type ){ + $result[$menu_id] = $menu_label; + } + } + return $result; + } + + + /** + * Send updated page manager content via ajax + * we're replacing more than just the content + * + */ + public function MenuJsonResponse($replace_id, $content){ + + $this->page->ajaxReplace[] = array('gp_menu_prep', '', ''); + $this->page->ajaxReplace[] = array('inner', $replace_id, $content); + $this->page->ajaxReplace[] = array('gp_menu_refresh', '', ''); + + ob_start(); + $this->HomepageDisplay(); + $content = ob_get_clean(); + $this->page->ajaxReplace[] = array('inner', '.homepage_setting', $content); + + ob_start(); + \gp\tool\Output::GetTopTwoMenu(); + $content = ob_get_clean(); + $this->page->ajaxReplace[] = array('inner', '#admin_menu_wrap', $content); + } + + + public function OutputMenu(){ + global $langmessage, $gp_titles, $gpLayouts; + + if( is_null($this->curr_menu_array) ){ + msg($langmessage['OOPS'] . ' (Current menu not set)'); + return; + } + + $array = $this->CurrMenuArray(); + $menu_keys = array_keys($array); + $menu_values = array_values($array); + + $curr_level = $menu_values[0]['level']; + + //for sites that don't start with level 0 + $prev_level = 0; + if( $curr_level > 0 ){ + $piece = '
  •  
      '; + while( $curr_level > $prev_level ){ + echo $piece; + $prev_level++; + } + } + + + foreach($menu_keys as $curr_key => $menu_key){ + + echo "\n"; + + $class = ''; + $menu_value = $menu_values[$curr_key]; + $curr_level = $menu_value['level']; + + + $next_level = 0; + if( isset($menu_values[$curr_key+1]) ){ + $next_level = $menu_values[$curr_key+1]['level']; + if( $next_level > $curr_level ){ + $class = 'haschildren'; + } + } + + if( isset($this->hidden_levels[$menu_key]) ){ + $class .= ' hidechildren'; + } + if( $curr_level >= $this->max_level_index){ + $class .= ' no-nest'; + } + + $class = \gp\admin\Menu\Tools::VisibilityClass($class, $menu_key); + + + //layout + $style = ''; + if( $this->is_main_menu ){ + if( isset($gp_titles[$menu_key]['gpLayout']) && isset($gpLayouts[$gp_titles[$menu_key]['gpLayout']]) ){ + $color = $gpLayouts[$gp_titles[$menu_key]['gpLayout']]['color']; + $style = 'background-color:' . $color . ';'; + } + } + + + echo '
    • '; + + $this->ShowLevel($menu_key, $menu_value); + + $this->EqualizeLevels($curr_level, $next_level); + } + + } + + + /** + * + * @param int $curr_level + * @param int $next_level + */ + protected function EqualizeLevels($curr_level, $next_level){ + global $langmessage; + + if( $next_level > $curr_level ){ + + $piece = '
        '; + while( $next_level > $curr_level ){ + echo $piece; + $curr_level++; + $piece = '
      • ' + . '' + . $langmessage['page_deleted'] + . '' + . '

        ' . $langmessage['page_deleted'] . '

        ' + . '
          '; + } + + }elseif( $next_level <= $curr_level ){ + + while( $next_level < $curr_level ){ + echo '
        '; + $curr_level--; + } + echo '
      • '; + } + + return $curr_level; + } + + + /** + * Check the curr_menu_array + * Remove missing titles + * Fill with new array if empty + * + */ + private function CurrMenuArray(){ + global $gp_titles; + + $menu_adjustments = false; + $array = array(); + + //get array of titles and levels + foreach($this->curr_menu_array as $key => $info){ + if( !isset($info['level']) ){ + break; + } + + //remove deleted titles + if( !isset($gp_titles[$key]) && !isset($info['url']) && !isset($info['area']) ){ + $menu_adjustments = true; + continue; + } + + $array[$key] = $info; + } + + //if the menu is empty (because all the files in it were deleted elsewhere), recreate it with the home page + if( count($array) == 0 ){ + $array = \gp\admin\Menu\Tools::AltMenu_New(); + $menu_adjustments = true; + } + + if( $menu_adjustments ){ + $this->curr_menu_array = $array; + $this->SaveMenu(false); + } + + return $array; + } + + + /** + * Output a piece of the editable menu + * + */ + public function ShowLevel($menu_key,$menu_value){ + global $gp_titles, $gpLayouts; + + $layout = \gp\admin\Menu\Tools::CurrentLayout($menu_key); + $layout_info = $gpLayouts[$layout]; + + echo '
        '; + + $style = ''; + $class = 'expand_img'; + if( !empty($gp_titles[$menu_key]['gpLayout']) ){ + $style = 'style="background-color:' . $layout_info['color'] . ';"'; + $class .= ' haslayout'; + } + + echo ''; + + if( isset($gp_titles[$menu_key]) ){ + $this->ShowLevel_Title($menu_key, $menu_value, $layout_info); + }elseif( isset($menu_value['url']) ){ + $this->ShowLevel_External($menu_key, $menu_value); + }elseif( isset($menu_value['area']) ){ + $this->ShowLevel_Extra($menu_key, $menu_value); + } + echo '
        '; + } + + + /** + * Show a menu entry if it's an Extra Content Area + * + */ + public function ShowLevel_Extra($menu_key, $menu_value){ + $data = array( + 'key' => $menu_key, + 'area' => $menu_value['area'], + 'label' => $menu_value['label'], + 'level' => $menu_value['level'], + ); + + if( strlen($data['label']) > 30 ){ + $data['title'] = substr($data['title'], 0, 30) . '...'; + } + + \gp\admin\Menu\Tools::MenuLink($data,'extra'); + echo \gp\tool::LabelSpecialChars($data['label']); + echo ''; + } + + + public function MenuSkeletonExtra(){ + global $langmessage; + + echo '' . $langmessage['options'] . ''; + echo ''; + + $img = ''; + $label = $langmessage['Menu Output'] . ' - ' . $langmessage['Classes']; + $attrs = array('title'=>$label, 'data-cmd'=>'gpabox'); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $label, + 'cmd=ClassesForm&index=[key]&no_a_classes=1', + $attrs + ); + + $img = ''; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['rm_from_menu'], + 'cmd=hide&index=[key]', + array( + 'title' => $langmessage['rm_from_menu'], + 'data-cmd' => 'postlink', + 'class' => 'gpconfirm' + ) + ); + + echo ''; + + $this->InsertLinks(); + } + + + /** + * Show a menu entry if it's an external link + * + */ + public function ShowLevel_External($menu_key, $menu_value){ + + $data = array( + 'key' => $menu_key, + 'url' => $menu_value['url'], + 'title' => $menu_value['url'], + 'level' => $menu_value['level'] + ); + + if( strlen($data['title']) > 30 ){ + $data['title'] = substr($data['title'],0,30).'...'; + } + + \gp\admin\Menu\Tools::MenuLink($data,'external'); + echo \gp\tool::LabelSpecialChars($menu_value['label']); + echo ''; + } + + + public function MenuSkeletonExtern(){ + global $langmessage; + + echo ''.$langmessage['Target URL'].''; + echo ''; + echo '[title]'; + echo ''; + + echo '' . $langmessage['options'] . ''; + echo ''; + + $img = ''; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['edit'], + 'cmd=EditExternal&key=[key]', + array('title'=>$langmessage['edit'],'data-cmd'=>'gpabox') + ); + + $img = ''; + $label = $langmessage['Menu Output'] . ' - ' . $langmessage['Classes']; + $attrs = array( + 'title' => $label, + 'data-cmd' =>'gpabox' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $label, + 'cmd=ClassesForm&index=[key]', + $attrs + ); + + $img = ''; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['rm_from_menu'], + 'cmd=hide&index=[key]', + array( + 'title' => $langmessage['rm_from_menu'], + 'data-cmd' => 'postlink', + 'class' => 'gpconfirm' + ) + ); + + echo ''; + + $this->InsertLinks(); + } + + + /** + * Show a menu entry if it's an internal page + * + */ + public function ShowLevel_Title($menu_key, $menu_value, $layout_info){ + + $title = \gp\tool::IndexToTitle($menu_key); + $data = $this->GetReplaceData($title, $layout_info, $menu_key, $menu_value); + $label = \gp\tool::GetLabel($title); + + \gp\admin\Menu\Tools::MenuLink($data); + echo \gp\tool::LabelSpecialChars($label); + echo ''; + } + + + /** + * Get the output formatting data for + * + */ + public function GetReplaceData($title, $layout_info, $menu_key, $menu_value=array() ){ + global $langmessage, $gp_titles; + + $isSpecialLink = \gp\tool::SpecialOrAdmin($title); + + //get the data for this title + $data = array( + 'key' => $menu_key, + 'url' => \gp\tool::GetUrl($title), + 'history' => \gp\tool::GetUrl('Admin/Revisions/').$menu_key, + 'title' => $title, + 'special' => $isSpecialLink, + 'has_layout' => !empty($gp_titles[$menu_key]['gpLayout']), + 'layout_color' => $layout_info['color'], + 'layout_label' => $layout_info['label'], + 'types' => implode(', ', explode(',', $gp_titles[$menu_key]['type'])), + 'opts' => '', + 'size' => '', + 'mtime' => '', + ); + + + if( isset($menu_value['level']) ){ + $data['level'] = $menu_value['level']; + } + + if( $isSpecialLink === false ){ + $file = \gp\tool\Files::PageFile($title); + $stats = @stat($file); + if( $stats ){ + $data['size'] = \gp\admin\Tools::FormatBytes($stats['size']); + $data['time'] = \gp\tool::date($langmessage['strftime_datetime'], $stats['mtime']); + } + } + + ob_start(); + \gp\tool\Plugins::Action( + 'MenuPageOptions', + array($title, $menu_key, $menu_value, $layout_info) + ); + $menu_options = ob_get_clean(); + if( $menu_options ){ + $data['opts'] = $menu_options; + } + + return $data; + } + + + /** + * Output html for the menu editing options displayed for selected titles + * + */ + public function MenuSkeleton(){ + global $langmessage; + + //page options + echo '' . $langmessage['page_options'] . ''; + + echo ''; + + $img = ''; + echo ''; + echo $img . htmlspecialchars($langmessage['view/edit_page']); + echo ''; + + $img = ''; + $attrs = array( + 'title' => $langmessage['rename/details'], + 'data-cmd' => 'gpajax', + 'class' => 'not_multiple' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['rename/details'], + 'cmd=renameform&index=[key]', + $attrs + ); + + + $img = ''; + $q = 'cmd=ToggleVisibility&index=[key]'; + $label = $langmessage['Visibility'] . ': ' . $langmessage['Private']; + $attrs = array( + 'title' => $label, + 'data-cmd' => 'postlink', + 'class' => 'vis_private' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $label, + $q, + $attrs + ); + + $img = ''; + $label = $langmessage['Visibility'] . ': ' . $langmessage['Public']; + $attrs = array( + 'title' => $label, + 'data-cmd' => 'postlink', + 'class' => 'vis_public not_multiple' + ); + $q .= '&visibility=private'; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $label, + $q, + $attrs + ); + + + echo ''; + echo ''; + echo htmlspecialchars($langmessage['Revision History']); + echo ''; + + + $img = ''; + $attrs = array( + 'title' => $langmessage['Copy'], + 'data-cmd' => 'gpabox', + 'class' => 'not_multiple not_special' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['Copy'], + 'cmd=CopyForm&index=[key]', + $attrs + ); + + + if( \gp\admin\Tools::HasPermission('Admin_User') ){ + $img = ''; + $attrs = array( + 'title' => $langmessage['permissions'], + 'data-cmd' => 'gpabox' + ); + echo $this->Link( + 'Admin/Permissions', + $img . $langmessage['permissions'], + 'index=[key]', + $attrs + ); + } + + + $img = ''; + $label = $langmessage['Menu Output'] . ' - ' . $langmessage['Classes']; + $attrs = array( + 'title' => $label, + 'data-cmd' => 'gpabox' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $label, + 'cmd=ClassesForm&index=[key]', + $attrs + ); + + + $img = ''; + $attrs = array( + 'title' => $langmessage['rm_from_menu'], + 'data-cmd' => 'postlink', + 'class' => 'gpconfirm' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['rm_from_menu'], + 'cmd=hide&index=[key]', + $attrs + ); + + + $img = ''; + $attrs = array( + 'title' => $langmessage['delete_page'], + 'data-cmd' => 'postlink', + 'class' => 'gpconfirm not_special' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['delete'], + 'cmd=MoveToTrash&index=[key]', + $attrs + ); + + + echo '[opts]'; // replaced with the contents of + // \gp\tool\Plugins::Action( + // 'MenuPageOptions', + // array($title, $menu_key, $menu_value, $layout_info) + // ); + + echo ''; + + //layout + if( $this->is_main_menu ){ + echo '
        '; + echo '' . $langmessage['layout'] . ''; + echo ''; + + //has_layout + $img = ''; + echo $this->Link( + 'Admin/Menu', + $img . '[layout_label]', + 'cmd=layout&index=[key]', + array( + 'data-cmd' => 'gpabox', + 'title' => $langmessage['layout'], + 'class' => 'has_layout' + ) + ); + + $img = ''; + echo $this->Link( + 'Admin/Menu', + $img . $langmessage['restore'], + 'cmd=restorelayout&index=[key]', + array( + 'data-cmd' => 'postlink', + 'title' => $langmessage['restore'], + 'class' => 'has_layout' + ), + 'restore' + ); + + //no_layout + $img = ''; + echo $this->Link( + 'Admin/Menu', + $img . '[layout_label]', + 'cmd=layout&index=[key]', + array( + 'data-cmd' => 'gpabox', + 'title' => $langmessage['layout'], + 'class' => 'no_layout' + ) + ); + echo ''; + echo '
        '; + } + + $this->InsertLinks(); + + //file stats + echo ''; + + } + + + + /** + * Output Insert links displayed with page options + * + */ + public function InsertLinks(){ + global $langmessage; + + echo '
        '; + echo ''.$langmessage['insert_into_menu'].''; + echo ''; + + $img = ''; + $query = 'cmd=insert_before&insert_where=[key]'; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['insert_before'], + $query, + array( + 'title' => $langmessage['insert_before'], + 'data-cmd' => 'gpabox' + ) + ); + + + $img = ''; + $query = 'cmd=insert_after&insert_where=[key]'; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['insert_after'], + $query, + array( + 'title' => $langmessage['insert_after'], + 'data-cmd' => 'gpabox' + ) + ); + + + $img = ''; + $query = 'cmd=insert_child&insert_where=[key]'; + echo $this->Link( + 'Admin/Menu/Ajax', + $img . $langmessage['insert_child'], + $query, + array( + 'title' => $langmessage['insert_child'], + 'data-cmd' => 'gpabox', + 'class' => 'insert_child' + ) + ); + echo ''; + echo '
        '; + } + + + + public function SearchDisplay(){ + global $langmessage, $gpLayouts, $gp_index, $gp_menu; + + $this->inherit_info = \gp\admin\Menu\Tools::Inheritance_Info(); + + switch($this->curr_menu_id){ + case 'search': + $show_list = $this->GetSearchList(); + break; + case 'hidden': + $show_list = \gp\admin\Menu\Tools::GetAvailable(); + break; + case 'nomenus': + $show_list = $this->GetNoMenus(); + break; + default: + $show_list = array_keys($gp_index); + break; + } + + $show_list = array_values($show_list); // to reset the keys + $show_list = array_reverse($show_list); //show newest first + $max = count($show_list); + while( ($this->search_page * $this->search_max_per_page) > $max ){ + $this->search_page--; + } + $start = $this->search_page * $this->search_max_per_page; + $stop = min(($this->search_page + 1) * $this->search_max_per_page, $max); + + + ob_start(); + echo ''; + $links = ob_get_clean(); + + echo $links; + + echo ''; + echo ''; + echo ''; + echo ''; + + + echo ''; + + if( count($show_list) > 0 ){ + for( $i = $start; $i < $stop; $i++ ){ + $title = $show_list[$i]; + $this->SearchDisplayRow($title); + } + } + + echo ''; + echo '
        '; + echo $langmessage['file_name']; + echo ''; + echo $langmessage['Content Type']; + echo ''; + echo $langmessage['Child Pages']; + echo ''; + echo $langmessage['File Size']; + echo ''; + echo $langmessage['Modified']; + echo '
        '; + + if( count($show_list) == 0 ){ + echo '

        '; + echo $langmessage['Empty']; + echo '

        '; + } + + echo '
        '; + echo $links; + } + + + + /** + * Get a list of titles matching the search criteria + * + */ + public function GetSearchList(){ + global $gp_index; + + + $key =& $_REQUEST['q']; + + if( empty($key) ){ + return array(); + } + + $key = strtolower($key); + $show_list = array(); + foreach($gp_index as $title => $index ){ + + if( strpos(strtolower($title),$key) !== false ){ + $show_list[$index] = $title; + continue; + } + + $label = \gp\tool::GetLabelIndex($index); + if( strpos(strtolower($label),$key) !== false ){ + $show_list[$index] = $title; + continue; + } + } + return $show_list; + } + + + + /** + * Get an array of titles that is not represented in any of the menus + * + */ + public function GetNoMenus(){ + global $gp_index; + + + //first get all titles in a menu + $menus = $this->GetAvailMenus('menu'); + $all_keys = array(); + foreach($menus as $menu_id => $label){ + $menu_array = \gp\tool\Output\Menu::GetMenuArray($menu_id); + $keys = array_keys($menu_array); + $all_keys = array_merge($all_keys,$keys); + } + $all_keys = array_unique($all_keys); + + //then check $gp_index agains $all_keys + $avail = array(); + foreach( $gp_index as $title => $index ){ + if( in_array($index, $all_keys) ){ + continue; + } + $avail[] = $title; + } + return $avail; + } + + + + /** + * Display row + * + */ + public function SearchDisplayRow($title){ + global $langmessage, $gpLayouts, $gp_index, $gp_menu, $gp_titles; + + $menu_key = $gp_index[$title]; + $layout = \gp\admin\Menu\Tools::CurrentLayout($menu_key); + $layout_info = $gpLayouts[$layout]; + $label = \gp\tool::GetLabel($title); + $data = $this->GetReplaceData($title, $layout_info, $menu_key); + + echo ''; + echo \gp\tool::Link($title,\gp\tool::LabelSpecialChars($label)); + + //area only display on mouseover + echo '
        '; + + echo $this->Link( + 'Admin/Menu/Ajax', + $langmessage['rename/details'], + 'cmd=renameform&index=' . urlencode($menu_key), + array( + 'title' => $langmessage['rename/details'], + 'data-cmd' => 'gpajax' + ) + ); + + $label = $langmessage['Visibility'] . ': ' . $langmessage['Private']; + $q = 'cmd=ToggleVisibility&index=' . urlencode($menu_key); + if( !isset($gp_titles[$menu_key]['vis']) ){ + $label = $langmessage['Visibility'] . ': ' . $langmessage['Public']; + $q .= '&visibility=private'; + } + $attrs = array( + 'title' => $label, + 'data-cmd' => 'postlink', // ######################## <<<<---- ######################## + // 'class' => 'vis_private' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $label, + $q, + $attrs + ); + + + if( $data['special'] === false ){ + echo \gp\tool::Link( + 'Admin/Revisions/'.$menu_key, + $langmessage['Revision History'], + 'cmd=ViewHistory', + 'class="view_edit_link not_multiple"' + ); + echo $this->Link( + 'Admin/Menu/Ajax', + $langmessage['Copy'], + 'cmd=CopyForm&index=' . urlencode($menu_key), + array( + 'title' => $langmessage['Copy'], + 'data-cmd' => 'gpabox' + ) + ); + } + + + echo ''; + echo $langmessage['layout'].': '; + echo $this->Link( + 'Admin/Menu', + $layout_info['label'], + 'cmd=layout&index=' . urlencode($menu_key), + array( + 'title' => $langmessage['layout'], + 'data-cmd' => 'gpabox' + ) + ); + echo ''; + + if( $data['special'] === false ){ + echo $this->Link( + 'Admin/Menu/Ajax', + $langmessage['delete'], + 'cmd=MoveToTrash&index=' . urlencode($menu_key), + array( + 'title' => $langmessage['delete_page'], + 'data-cmd' => 'postlink', + 'class' => 'gpconfirm' + ) + ); + } + + echo $data['opts']; + + //stats + if( gpdebug ){ + echo 'Data Index: '.$menu_key.''; + } + echo '
         
        '; + + //types + echo ''; + $this->TitleTypes($menu_key); + + //children + echo ''; + if( isset($this->inherit_info[$menu_key]) && isset($this->inherit_info[$menu_key]['children']) ){ + echo $this->inherit_info[$menu_key]['children']; + }elseif( isset($gp_menu[$menu_key]) ){ + echo '0'; + }else{ + echo $langmessage['Not In Main Menu']; + } + + //size, modified + echo ''; + echo $data['size']; + echo ''; + echo $data['mtime']; + echo ''; + } + + + + /** + * List section types + * + */ + public function TitleTypes($title_index){ + global $gp_titles; + + $types = explode(',',$gp_titles[$title_index]['type']); + $types = array_filter($types); + $types = array_unique($types); + + foreach( $types as $i => $type ){ + if( isset($this->section_types[$type]) && isset($this->section_types[$type]['label']) ){ + $types[$i] = $this->section_types[$type]['label']; + } + } + + echo implode(', ', $types); + } + + + /** + * Get a list of pages that are not in the current menu array + * @return array + */ + protected function GetAvail_Current(){ + global $gp_index; + + if( $this->is_main_menu ){ + return \gp\admin\Menu\Tools::GetAvailable(); + } + + $avail = array(); + foreach( $gp_index as $title => $index ){ + if( !isset($this->curr_menu_array[$index]) ){ + $avail[$index] = $title; + } + } + return $avail; + } + + + /** + * Save changes to the current menu array after a drag event occurs + * @return bool + */ + public function SaveDrag(){ + global $langmessage; + + $this->CacheSettings(); + if( is_null($this->curr_menu_array) ){ + msg($langmessage['OOPS'].'(1)'); + return false; + } + + $key = $_POST['drag_key']; + if( !isset($this->curr_menu_array[$key]) ){ + msg($langmessage['OOPS'].' (Unknown menu key)'); + return false; + } + + + $moved = $this->RmMoved($key); + if( !$moved ){ + msg($langmessage['OOPS'].'(3)'); + return false; + } + + + // if prev (sibling) set + if( !empty($_POST['prev']) ){ + + $inserted = $this->MenuInsert_After( $moved, $_POST['prev']); + + // if parent is set + }elseif( !empty($_POST['parent']) ){ + + $inserted = $this->MenuInsert_Child( $moved, $_POST['parent']); + + // if no siblings, no parent then it's the root + }else{ + $inserted = $this->MenuInsert_Before( $moved, false); + + } + + if( !$inserted ){ + $this->RestoreSettings(); + msg($langmessage['OOPS'].'(4)'); + return; + } + + if( !$this->SaveMenu(false) ){ + $this->RestoreSettings(); + \gp\tool::AjaxWarning(); + return false; + } + + } + + + + /** + * Get portion of menu that was moved + */ + public function RmMoved($key){ + if( !isset($this->curr_menu_array[$key]) ){ + return false; + } + + $old_level = false; + $moved = array(); + + foreach($this->curr_menu_array as $menu_key => $info){ + + if( !isset($info['level']) ){ + break; + } + $level = $info['level']; + + if( $old_level === false ){ + + if( $menu_key != $key ){ + continue; + } + + $old_level = $level; + $moved[$menu_key] = $info; + unset($this->curr_menu_array[$menu_key]); + continue; + } + + if( $level <= $old_level ){ + break; + } + + $moved[$menu_key] = $info; + unset($this->curr_menu_array[$menu_key]); + } + return $moved; + } + + + + /** + * Remove key from curr_menu_array + * Adjust children levels if necessary + * + */ + protected function RmFromMenu($search_key,$curr_menu=true){ + global $gp_menu; + + if( $curr_menu ){ + $keys = array_keys($this->curr_menu_array); + $values = array_values($this->curr_menu_array); + }else{ + $keys = array_keys($gp_menu); + $values = array_values($gp_menu); + } + + $insert_key = array_search($search_key,$keys); + if( ($insert_key === null) || ($insert_key === false) ){ + return false; + } + + unset($keys[$insert_key]); + $keys = array_values($keys); + + unset($values[$insert_key]); + $values = array_values($values); + + + //adjust levels of children + $prev_level = -1; + if( isset($values[$insert_key-1]) ){ + $prev_level = $values[$insert_key-1]['level']; + } + + do{ + $moved_one = false; + if( isset($values[$insert_key]) ){ + $curr_level = $values[$insert_key]['level']; + if( ($prev_level+1) < $curr_level ){ + $values[$insert_key]['level']--; + $prev_level = $values[$insert_key]['level']; + $moved_one = true; + $insert_key++; + } + } + }while($moved_one); + + //shouldn't happen + if( count($keys) == 0 ){ + return false; + } + + //rebuild + if( $curr_menu ){ + $this->curr_menu_array = array_combine($keys, $values); + }else{ + $gp_menu = array_combine($keys, $values); + } + + return true; + } + + + + /** + * Insert titles into menu + * + */ + protected function MenuInsert_Before($titles,$sibling){ + + $old_level = \gp\admin\Menu\Tools::GetRootLevel($titles); + + //root install + if( $sibling === false ){ + $level_adjustment = 0 - $old_level; + $titles = $this->AdjustMovedLevel($titles,$level_adjustment); + $this->curr_menu_array = $titles + $this->curr_menu_array; + return true; + } + + //before sibling + if( !isset($this->curr_menu_array[$sibling]) || !isset($this->curr_menu_array[$sibling]['level']) ){ + return false; + } + + $sibling_level = $this->curr_menu_array[$sibling]['level']; + $level_adjustment = $sibling_level - $old_level; + $titles = $this->AdjustMovedLevel($titles,$level_adjustment); + + $new_menu = array(); + foreach($this->curr_menu_array as $menu_key => $menu_info ){ + + if( $menu_key == $sibling ){ + foreach($titles as $titles_key => $titles_info){ + $new_menu[$titles_key] = $titles_info; + } + } + $new_menu[$menu_key] = $menu_info; + } + $this->curr_menu_array = $new_menu; + return true; + } + + + + /* + * Insert $titles into $menu as siblings of $sibling + * Place + * + */ + protected function MenuInsert_After($titles,$sibling,$level_adjustment=0){ + + if( !isset($this->curr_menu_array[$sibling]) || !isset($this->curr_menu_array[$sibling]['level']) ){ + return false; + } + + $sibling_level = $this->curr_menu_array[$sibling]['level']; + + //level adjustment + $old_level = \gp\admin\Menu\Tools::GetRootLevel($titles); + $level_adjustment += $sibling_level - $old_level; + $titles = $this->AdjustMovedLevel($titles,$level_adjustment); + + // rebuild menu + // insert $titles after sibling and it's children + $new_menu = array(); + $found_sibling = false; + foreach($this->curr_menu_array as $menu_key => $menu_info){ + + $menu_level = 0; + if( isset($menu_info['level']) ){ + $menu_level = $menu_info['level']; + } + + if( $found_sibling && ($menu_level <= $sibling_level) ){ + foreach($titles as $titles_key => $titles_info){ + $new_menu[$titles_key] = $titles_info; + } + $found_sibling = false; //prevent multiple insertions + } + + $new_menu[$menu_key] = $menu_info; + + if( $menu_key == $sibling ){ + $found_sibling = true; + } + } + + //if it's added to the end + if( $found_sibling ){ + foreach($titles as $titles_key => $titles_info){ + $new_menu[$titles_key] = $titles_info; + } + } + $this->curr_menu_array = $new_menu; + + return true; + } + + + + /* + * Insert $titles into $menu as children of $parent + * + */ + protected function MenuInsert_Child($titles,$parent){ + + if( !isset($this->curr_menu_array[$parent]) || !isset($this->curr_menu_array[$parent]['level']) ){ + return false; + } + + $parent_level = $this->curr_menu_array[$parent]['level']; + + //level adjustment + $old_level = \gp\admin\Menu\Tools::GetRootLevel($titles); + $level_adjustment = $parent_level - $old_level + 1; + $titles = $this->AdjustMovedLevel($titles,$level_adjustment); + + //rebuild menu + // insert $titles after parent + $new_menu = array(); + foreach($this->curr_menu_array as $menu_title => $menu_info){ + $new_menu[$menu_title] = $menu_info; + + if( $menu_title == $parent ){ + foreach($titles as $titles_title => $titles_info){ + $new_menu[$titles_title] = $titles_info; + } + } + } + + $this->curr_menu_array = $new_menu; + return true; + } + + + + protected function AdjustMovedLevel($titles,$level_adjustment){ + + foreach($titles as $title => $info){ + $level = 0; + if( isset($info['level']) ){ + $level = $info['level']; + } + $titles[$title]['level'] = min($this->max_level_index,$level + $level_adjustment); + } + return $titles; + } + + + + /** + * Display the current homepage setting + * + */ + public function HomepageDisplay(){ + global $langmessage, $config; + + if( \gp\admin\Menu\Tools::ResetHomepage() ){ + \gp\admin\Tools::SaveConfig(); + } + + $label = \gp\tool::GetLabelIndex($config['homepath_key']); + + echo ' '; + echo $langmessage['Homepage'] . ': '; + echo \gp\tool::Link( + 'Admin/Menu/Ajax', + $label, + 'cmd=HomepageSelect', + 'data-cmd="gpabox"' + ); + if( empty($config['homepath_auto']) ){ + echo '  '; + echo \gp\tool::Link( + 'Admin/Menu/Ajax', + '', + 'cmd=HomepageAuto', + array( + 'class' => 'gpbutton', + 'title' => $langmessage['disable'], + 'data-cmd' => 'gpajax', + ) + ); + } + } + + + + public function CacheSettings(){ + global $gp_index, $gp_titles, $gp_menu; + + $this->settings_cache['gp_index'] = $gp_index; + $this->settings_cache['gp_titles'] = $gp_titles; + $this->settings_cache['gp_menu'] = $gp_menu; + + if( !$this->is_main_menu ){ + $this->settings_cache['curr_menu_array'] = $this->curr_menu_array; + } + } + + + + public function RestoreSettings(){ + global $gp_index, $gp_titles, $gp_menu; + + + if( isset($this->settings_cache['gp_titles']) ){ + $gp_titles = $this->settings_cache['gp_titles']; + } + + if( isset($this->settings_cache['gp_menu']) ){ + $gp_menu = $this->settings_cache['gp_menu']; + } + + if( isset($this->settings_cache['gp_index']) ){ + $gp_index = $this->settings_cache['gp_index']; + } + + if( isset($this->settings_cache['curr_menu_array']) ){ + $this->curr_menu_array = $this->settings_cache['curr_menu_array']; + } + } + + +} diff --git a/include/admin/Menu/Ajax.php b/include/admin/Menu/Ajax.php new file mode 100644 index 0000000..da0f701 --- /dev/null +++ b/include/admin/Menu/Ajax.php @@ -0,0 +1,1376 @@ +cmd === 'return' ){ + return; + } + + switch($this->cmd){ + + //adding new files + case 'AddHidden': + $this->AddHidden(); + return; + + case 'CopyForm': + $this->CopyForm(); + return; + + case 'CopyPage': + $this->CopyPage(); + break; + + + // Page Insertion + case 'insert_before': + case 'insert_after': + case 'insert_child': + $this->InsertDialog(); + return; + + case 'NewFile': + $this->NewFile(); + break; + + case 'InsertFromHidden'; + $this->InsertFromHidden(); + break; + + case 'RestoreFromTrash': + $this->RestoreFromTrash(); + break; + + + //external links + case 'NewExternal': + $this->NewExternal(); + break; + case 'EditExternal': + $this->EditExternal(); + return; + case 'SaveExternal': + $this->SaveExternal(); + break; + + + //extra area + case 'InsertExtra': + $this->InsertExtra(); + break; + + + //edit classes + case 'ClassesForm': + $this->ClassesForm(); + break; + case 'SaveClasses': + $this->SaveClasses(); + break; + + + //menu editing + case 'hide': + $this->Hide(); + break; + case 'MoveToTrash': + $this->MoveToTrash(); + break; + + + //rename + case 'renameform': + $this->RenameForm(); //will die() + return; + case 'RenameFile': + $this->RenameFile(); + break; + + //visibility + case 'ToggleVisibility': + $this->ToggleVisibility(); + break; + + //homepage + case 'HomepageSelect': + $this->HomepageSelect(); + return; + case 'HomepageSave': + $this->HomepageSave(); + return; + case 'HomepageAuto': + $this->HomepageAuto(); + return; + } + + parent::RunScript(); + } + + + /** + * Display a user form for adding a new page that won't be immediately added to a menu + * + */ + public function AddHidden(){ + global $langmessage, $gp_index; + + $_REQUEST += array('title' => ''); + $_REQUEST['gpx_content'] = 'gpabox'; + + echo '
        '; + + echo '

        '.$langmessage['new_file'].'

        '; + + echo '
        '; + if( isset($_REQUEST['redir']) ){ + echo ''; + } + + + echo ''; + echo ''; + + //title + $new_title = htmlspecialchars($_REQUEST['title']); + // prevent code injections + $new_title = str_replace(array('=', '/', '{', '}', ':', ',', ';'), '', $new_title); + echo ''; + + //copy + echo ''; + + + //new content type + echo ''; + + echo '
        ' . $langmessage['options'] . '
        '; + echo $langmessage['label']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['Copy']; + echo ''; + $gp_index_no_special = array(); + foreach( $gp_index as $title => $index ){ + if( strpos(strtolower($index), 'special_') !== 0 ){ + $gp_index_no_special[$title] = $index; + } + } + \gp\admin\Menu\Tools::ScrollList($gp_index_no_special); + echo '
        '; + echo str_replace(' ', ' ', $langmessage['Content Type']); + echo ''; + echo ''; + echo '
        '; + + echo '

        '; + echo ''; + echo ''; + echo ''; + echo '

        '; + echo '
        '; + echo '
        '; + echo ''; + + } + + + /** + * Message or redirect when file is saved + * + */ + public function HiddenSaved($new_index){ + global $langmessage; + + $this->search_page = 0; //take user back to first page where the new page will be displayed + + if( isset($_REQUEST['redir']) ){ + $title = \gp\tool::IndexToTitle($new_index); + $url = \gp\tool::AbsoluteUrl($title, '', true, false, true); + msg(sprintf($langmessage['will_redirect'], \gp\tool::Link_Page($title))); + $this->page->ajaxReplace[] = array('location', $url, 15000); + }else{ + msg($langmessage['SAVED']); + } + } + + + + /** + * Display a form for copying a page + * + */ + public function CopyForm(){ + global $langmessage, $gp_index; + + + $index = $_REQUEST['index']; + $from_title = \gp\tool::IndexToTitle($index); + + if( !$from_title ){ + msg($langmessage['OOPS_TITLE']); + return false; + } + + $from_label = \gp\tool::GetLabel($from_title); + $from_label = \gp\tool::LabelSpecialChars($from_label); + + echo '
        '; + echo '
        '; + if( isset($_REQUEST['redir']) ){ + echo ' '; + } + echo ' '; + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo '
        '; + echo $langmessage['Copy']; + echo '
        '; + echo $langmessage['from']; + echo ''; + echo $from_label; + echo '
        '; + echo $langmessage['to']; + echo ''; + echo ''; + echo '
        '; + + echo '

        '; + echo ' '; + echo ''; + echo ''; + echo '

        '; + + echo '
        '; + echo '
        '; + } + + + /** + * Perform a page copy + * + */ + public function CopyPage(){ + global $gp_index, $gp_titles, $langmessage, $users, $gpAdmin, $dataDir; + + $this->CacheSettings(); + + if( !isset($_POST['from_title']) ){ + msg($langmessage['OOPS'].' (Copy from not selected)'); + + if( isset($_POST['insert_how']) ){ + $this->InsertDialog($_POST['insert_how']); + }else{ + $this->AddHidden(); + } + + return false; + } + + //existing page info + $from_title = $_POST['from_title']; + if( !isset($gp_index[$from_title]) ){ + msg($langmessage['OOPS_TITLE']); + return false; + } + $from_index = $gp_index[$from_title]; + $info = $gp_titles[$from_index]; + + + //check the new title + $title = $_POST['title']; + $title = \gp\admin\Tools::CheckPostedNewPage($title,$message); + if( $title === false ){ + msg($message); + return false; + } + + //get the existing content + $from_file = \gp\tool\Files::PageFile($from_title); + $contents = file_get_contents($from_file); + + + //add to $gp_index first! + $index = \gp\tool::NewFileIndex(); + $gp_index[$title] = $index; + $file = \gp\tool\Files::PageFile($title); + + if( !\gp\tool\Files::Save($file,$contents) ){ + msg($langmessage['OOPS'].' (File not saved)'); + return false; + } + + //set permissions for copied page + // msg('gpAdmin = ' . pre($gpAdmin)); + $users = \gp\tool\Files::Get('_site/users'); + $username = $gpAdmin['username']; + $user_file = $dataDir . '/data/_sessions/' . $users[$username]['file_name']; + $editing_values = $gpAdmin['editing']; + if( $editing_values != 'all' && strpos($editing_values, ',' . $index . ',') === false ){ + $editing_values .= $index.','; + $gpAdmin['editing'] = $editing_values; + // save to user session file + \gp\tool\Files::SaveData($user_file, 'gpAdmin', $gpAdmin); + // save to users.php + $users[$username]['editing'] = $editing_values; + \gp\tool\Files::SaveData('_site/users', 'users', $users); + } + + //add to gp_titles + $new_titles = array(); + $new_titles[$index]['label'] = \gp\admin\Tools::PostedLabel($_POST['title']); + $new_titles[$index]['type'] = $info['type']; + $gp_titles += $new_titles; + + + //add to menu + $insert = array(); + $insert[$index] = array(); + + if( !$this->SaveNew($insert) ){ + $this->RestoreSettings(); + return false; + } + + + $this->HiddenSaved($index); + + return true; + } + + + + /** + * Display the dialog for inserting pages into a menu + * + */ + public function InsertDialog($cmd = null){ + global $langmessage, $gp_index; + + if( is_null($cmd) ){ + $cmd = $this->cmd; + } + + $_REQUEST['gpx_content'] = 'gpabox'; + + + //create format of each tab + ob_start(); + echo '
        '; + echo '
        '; + echo ''; + echo ''; + + // echo ''; + // echo ''; + // echo '
         
        '; + + $format_top = ob_get_clean(); + + ob_start(); + echo '

        '; + echo ''; + echo ''; + echo '

        '; + echo '
        '; + echo '
        '; + $format_bottom = ob_get_clean(); + + + + echo '
        '; + + //tabs + echo ''; + + + // Copy + echo sprintf($format_top,'gp_Insert_Copy',''); + echo ''; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['label']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['Copy']; + echo ''; + $copy_list = array(); + foreach($gp_index as $k => $v){ + if( strpos($v,'special_') === 0 ){ + continue; + } + $copy_list[$k] = $v; + } + \gp\admin\Menu\Tools::ScrollList($copy_list); + echo '
        '; + echo sprintf($format_bottom,'CopyPage',$langmessage['Copy']); + + + // Insert New + echo sprintf($format_top,'gp_Insert_New','nodisplay'); + echo ''; + echo ''; + + echo ''; + echo '
        '; + echo $langmessage['label']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['Content Type']; + echo ''; + echo ''; + echo '
        '; + echo sprintf($format_bottom,'NewFile', $langmessage['create_new_file']); + + + // Insert Hidden + $avail = $this->GetAvail_Current(); + + if( !empty($avail) ){ + echo sprintf($format_top, 'gp_Insert_Hidden', 'nodisplay'); + $avail = array_flip($avail); + \gp\admin\Menu\Tools::ScrollList($avail, 'keys[]', 'checkbox', true); + echo sprintf($format_bottom, 'InsertFromHidden', $langmessage['insert_into_menu']); + } + + + + // Insert Deleted / Restore from trash + $scroll_list = $this->TrashScrolllist(); + if( !empty($scroll_list) ){ + echo sprintf($format_top, 'gp_Insert_Deleted', 'nodisplay'); + echo $scroll_list; + echo sprintf($format_bottom, 'RestoreFromTrash', $langmessage['restore_from_trash']); + } + + + //Insert External + echo '
        '; + $args = array(); + $args['insert_how'] = $cmd; + $args['insert_where'] = $_REQUEST['insert_where']; + $this->ExternalForm('NewExternal', $langmessage['insert_into_menu'], $args); + echo '
        '; + + + //Insert Extra + $areas = $this->GetExtraAreas(); + // msg("Areas: " . pre($areas)); + if( !empty($areas) ){ + echo sprintf($format_top, 'gp_Insert_Extra', 'nodisplay'); + echo '

        '; + echo ''; + echo 'Outputs an Extra Content Area at the current position in the menu. '; + echo 'This way you may add anything from simple separators to subheads or even images or forms.
        '; + echo 'This is an advanced feature and requires specific custom CSS to be useful.

        '; + \gp\admin\Menu\Tools::ScrollListExtra($areas); + echo sprintf($format_bottom, 'InsertExtra', $langmessage['insert']); + } + + + echo '
        '; + + } + + + function GetExtraAreas(){ + global $dataDir; + $areas = array(); + $folder = $dataDir . '/data/_extra'; + $files = scandir($folder); + foreach($files as $file){ + $title = \gp\admin\Content\Extra::AreaExists($file); + if( $title === false ){ + continue; + } + $areas[$title] = str_replace('_', ' ', $title); + } + uksort($areas,'strnatcasecmp'); + return $areas; + } + + + + + /** + * Generate a scroll list selector for trash titles + * + */ + function TrashScrolllist(){ + global $langmessage; + + $trashtitles = \gp\admin\Content\Trash::TrashFiles(); + if( empty($trashtitles) ){ + return ''; + } + + ob_start(); + echo '
        '; + echo ''; + foreach($trashtitles as $title => $info){ + if( empty($info['label']) ){ + continue; + } + echo ''; + } + echo '
        '; + + return ob_get_clean(); + } + + + /** + * Create a new file + * + */ + public function NewFile(){ + global $langmessage; + $this->CacheSettings(); + + $new_index = \gp\admin\Menu\Tools::CreateNew(); + if( $new_index === false ){ + return false; + } + + $insert = array(); + $insert[$new_index] = array(); + + if( !$this->SaveNew($insert) ){ + $this->RestoreSettings(); + return false; + } + + $this->HiddenSaved($new_index); + } + + + /** + * Insert pages into the current menu from existing pages that aren't in the menu + * + */ + public function InsertFromHidden(){ + global $langmessage, $gp_index; + + if( is_null($this->curr_menu_array) ){ + msg($langmessage['OOPS'] . ' (Menu not set)'); + return false; + } + + $this->CacheSettings(); + + //get list of titles from submitted indexes + $titles = array(); + if( isset($_POST['keys']) ){ + foreach($_POST['keys'] as $index){ + if( \gp\tool::IndexToTitle($index) !== false ){ + $titles[$index]['level'] = 0; + } + } + } + + if( count($titles) == 0 ){ + msg($langmessage['OOPS'] . ' (Nothing selected)'); + $this->RestoreSettings(); + return false; + } + + if( !$this->SaveNew($titles) ){ + $this->RestoreSettings(); + return false; + } + + } + + + /** + * Add titles to the current menu from the trash + * + */ + public function RestoreFromTrash(){ + global $langmessage, $gp_index; + + + if( is_null($this->curr_menu_array) ){ + msg($langmessage['OOPS']); + return false; + } + + if( !isset($_POST['titles']) ){ + msg($langmessage['OOPS'] . ' (Nothing Selected)'); + return false; + } + + $this->CacheSettings(); + + $titles = array(); + $menu = \gp\admin\Content\Trash::RestoreTitles($_POST['titles']); + + + if( empty($menu) ){ + msg($langmessage['OOPS']); + $this->RestoreSettings(); + return false; + } + + + if( !$this->SaveNew($menu) ){ + $this->RestoreSettings(); + return false; + } + + \gp\admin\Content\Trash::ModTrashData(null, $titles); + } + + + /** + * Form for adding external link + * + */ + public function ExternalForm($cmd,$submit,$args){ + global $langmessage; + + //these aren't all required for each usage of ExternalForm() + $args += array( + 'url' =>'http://', + 'label' => '', + 'title_attr' => '', + 'insert_how' => '', + 'insert_where' => '', + 'key' => '', + ); + + + echo '
        '; + echo ''; + echo ''; + echo ''; + + echo ''; + + // echo ''; + // echo ''; + // echo ''; + // echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo ''; + echo '
          
        '; + echo $langmessage['Target URL']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['label']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['title attribute']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['New_Window']; + echo ''; + if( isset($args['new_win']) ){ + echo ''; + }else{ + echo ''; + } + echo '
        '; + + echo '

        '; + echo ''; + echo ' '; + echo ' '; + echo '

        '; + + echo '
        '; + } + + + /** + * Form for adding/editing custom CSS class names a menu item + * + */ + public function ClassesForm(){ + global $langmessage; // msg('ma = ' .pre($this->curr_menu_array)); + + if( !isset($_REQUEST['index']) || !isset($this->curr_menu_array[$_REQUEST['index']]) ){ + msg($langmessage['OOPS'] . ' (Invalid request or menu key)'); + return; + } + + $key = $_REQUEST['index']; + + $classes_li = ''; + if( isset($this->curr_menu_array[$key]['classes_li']) ){ + $classes_li = $this->curr_menu_array[$key]['classes_li']; + } + + if( !isset($_REQUEST['no_a_classes']) ){ + $classes_a = ''; + if( isset($this->curr_menu_array[$key]['classes_a']) ){ + $classes_a = $this->curr_menu_array[$key]['classes_a']; + } + } + + $classes_child_ul = ''; + if( isset($this->curr_menu_array[$key]['classes_child_ul']) ){ + $classes_child_ul = $this->curr_menu_array[$key]['classes_child_ul']; + } + + + echo '
        '; + echo '
        '; + echo ''; + + echo '

        ' . $langmessage['Menu Output'] . ' - ' . $langmessage['Classes'] . '

        '; + + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + if( !isset($_REQUEST['no_a_classes']) ){ + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
        Menu Element' . $langmessage['Classes'] . '
        li
        li > a
        li > ul
        '; + + echo '

        '; + echo ''; + echo ' '; + echo ' '; + echo '

        '; + + echo '
        '; + echo '
        '; + } + + + /** + * Save posted custom CSS class name(s) for menu item + * + */ + public function SaveClasses(){ + global $langmessage; + + if( isset($_POST['key']) && substr($_POST['key'], 0, 6) == '_extra'){ + $_POST['classes_a'] = ''; + } + + if( !isset($_POST['key']) || !isset($_POST['classes_li']) || !isset($_POST['classes_a']) || !isset($_POST['classes_child_ul']) ){ + msg($langmessage['OOPS'] . ' (Invalid request)'); + return; + } + + $key = $_POST['key']; + + if( !isset($this->curr_menu_array[$key]) ){ + msg($langmessage['OOPS'] . ' (Invalid menu key)'); + return; + } + + $this->CacheSettings(); + + $this->curr_menu_array[$key]['classes_li'] = $this->ValidClasses($_POST['classes_li']); + $this->curr_menu_array[$key]['classes_a'] = $this->ValidClasses($_POST['classes_a']); + $this->curr_menu_array[$key]['classes_child_ul'] = $this->ValidClasses($_POST['classes_child_ul']); + + if( !$this->SaveMenu(false) ){ + msg($langmessage['OOPS'] . ' (Menu Not Saved)'); + $this->RestoreSettings(); + return false; + } + + } + + + + /** + * Removes invalid CSS class names + * Returns only valid CSS class names + * Displays error/remove msg for invalid class names + * @param classes (space separated string or array) + * @return valid_classes (string or array, depending on passed argument type) + * + */ + public function ValidClasses($classes){ + global $langmessage; + + $arg_type = gettype($classes); + + if( $arg_type != 'string' && $arg_type != 'array' ){ + msg($langmessage['OOPS'] . ' (Wrong type ' . $arg_type . ', array or string expected)'); + return false; + } + if( empty($classes) ){ + return $classes; + } + + if( $arg_type == 'string' ){ + $classes = explode(' ', $classes); + } + + $valid_classes = array(); + foreach( $classes as $classname ){ + if( $classname == ' ' || empty($classname) ){ + // skip leftovers from multiple space chars + continue; + } + // $classname = trim($classname); + if( !preg_match("/^([a-z_]|-[a-z_-])[a-z\d_-]*$/i", $classname) ){ + msg('' . htmlspecialchars($classname) . ' is not a valid CSS class name and was removed.'); + continue; + } + $valid_classes[] = $classname; + } + + if( $arg_type == 'string' ){ + $valid_classes = implode(' ', $valid_classes); + } + + return $valid_classes; + } + + + + /** + * Place an Extra Content Area inside the current menu + * + */ + public function InsertExtra(){ + global $gp_menu, $langmessage; + + $this->CacheSettings(); + $area = $_POST['from_extra']; + + if( !\gp\admin\Content\Extra::AreaExists($area) ){ + msg($langmessage['OOPS'] . ' (Extra Area does not exist)'); + return; + } + + $key = $this->NewExtraKey(); + $insert = array(); + $insert[$key] = array( + 'area' => $area, + 'label' => str_replace('_', ' ', $area), + ); + + if( !$this->SaveNew($insert) ){ + msg($langmessage['OOPS'] . ' (Adding Extra Content Area failed)'); + $this->RestoreSettings(); + return false; + } + } + + + public function NewExtraKey(){ + $num_index = 0; + do{ + $new_key = '_extra_' . base_convert($num_index, 10, 36); + $num_index++; + }while( isset($this->curr_menu_array[$new_key]) ); + + return $new_key; + } + + + /** + * Save a new external link in the current menu + * + */ + public function NewExternal(){ + global $langmessage; + + $this->CacheSettings(); + $array = $this->ExternalPost(); + + if( !$array ){ + msg($langmessage['OOPS'] . ' (Invalid Request)'); + return; + } + + $key = $this->NewExternalKey(); + $insert = array(); + $insert[$key] = $array; + + if( !$this->SaveNew($insert) ){ + $this->RestoreSettings(); + return false; + } + } + + + /** + * Edit an external link entry in the current menu + * + */ + public function EditExternal(){ + global $langmessage; + + $key =& $_REQUEST['key']; + if( !isset($this->curr_menu_array[$key]) ){ + msg($langmessage['OOPS'] . ' (Current menu not set)'); + return false; + } + + $info = $this->curr_menu_array[$key]; + $info['key'] = $key; + + echo '
        '; + + echo '

        ' . $langmessage['External Link'] . '

        '; + + $this->ExternalForm('SaveExternal', $langmessage['save'], $info); + + echo '
        '; + } + + + /** + * Save changes to an external link entry in the current menu + * + */ + public function SaveExternal(){ + global $langmessage; + + $key =& $_POST['key']; + if( !isset($this->curr_menu_array[$key]) ){ + msg($langmessage['OOPS'] . ' (Current menu not set)'); + return false; + } + $level = $this->curr_menu_array[$key]['level']; + + $array = $this->ExternalPost(); + if( !$array ){ + msg($langmessage['OOPS'] . ' (1)'); + return; + } + + $this->CacheSettings(); + + $array['level'] = $level; + $this->curr_menu_array[$key] = $array; + + if( !$this->SaveMenu(false) ){ + msg($langmessage['OOPS'] . ' (Menu Not Saved)'); + $this->RestoreSettings(); + return false; + } + + } + + + /** + * Check the values of a post with external link values + * + */ + public function ExternalPost(){ + + $array = array(); + if( empty($_POST['url']) || $_POST['url'] == 'http://' ){ + return false; + } + $array['url'] = htmlspecialchars($_POST['url']); + $array['label'] = \gp\admin\Tools::PostedLabel($_POST['label']); + + if( empty($array['label']) ){ + return false; + } + + if( !empty($_POST['title_attr']) ){ + $array['title_attr'] = htmlspecialchars($_POST['title_attr']); + } + if( isset($_POST['new_win']) && $_POST['new_win'] == 'new_win' ){ + $array['new_win'] = true; + } + return $array; + } + + public function NewExternalKey(){ + + $num_index = 0; + do{ + $new_key = '_' . base_convert($num_index,10,36); + $num_index++; + }while( isset($this->curr_menu_array[$new_key]) ); + + return $new_key; + } + + + /** + * Save pages + * + * @param array $titles + * @return bool + */ + protected function SaveNew($titles){ + global $langmessage; + + //menu modification + if( isset($_POST['insert_where']) && isset($_POST['insert_how']) ){ + + if( !$this->MenuInsert($titles, $_POST['insert_where'], $_POST['insert_how']) ){ + msg($langmessage['OOPS'] . ' (Insert Failed)'); + return false; + } + + if( !$this->SaveMenu(true) ){ + msg($langmessage['OOPS'] . ' (Menu Not Saved)'); + return false; + } + + return true; + } + + + if( !\gp\admin\Tools::SavePagesPHP(true) ){ + return false; + } + + return true; + } + + + /** + * Insert titles into the current menu if needed + * + */ + public function MenuInsert($titles, $neighbor, $insert_how){ + switch($insert_how){ + case 'insert_before': + return $this->MenuInsert_Before($titles, $neighbor); + + case 'insert_after': + return $this->MenuInsert_After($titles, $neighbor); + + case 'insert_child': + return $this->MenuInsert_After($titles, $neighbor, 1); + } + + return false; + } + + + /** + * Remove from the menu + * + */ + public function Hide(){ + global $langmessage; + + if( is_null($this->curr_menu_array) ){ + msg($langmessage['OOPS'] . '(1)'); + return false; + } + + $this->CacheSettings(); + + $_POST += array('index' => ''); + $indexes = explode(',', $_POST['index']); + + foreach($indexes as $index ){ + + if( count($this->curr_menu_array) == 1 ){ + break; + } + + if( !isset($this->curr_menu_array[$index]) ){ + msg($langmessage['OOPS'] . '(3)'); + return false; + } + + if( !$this->RmFromMenu($index) ){ + msg($langmessage['OOPS'] . '(4)'); + $this->RestoreSettings(); + return false; + } + } + + if( $this->SaveMenu(false) ){ + return true; + } + + msg($langmessage['OOPS'] . '(5)'); + $this->RestoreSettings(); + return false; + } + + + /** + * Move To Trash + * Hide special pages + * + */ + public function MoveToTrash(){ + global $gp_titles, $gp_index, $langmessage, $gp_menu, $config, $dataDir; + + $this->CacheSettings(); + + $_POST += array('index'=>''); + $indexes = explode(',', $_POST['index']); + $trash_data = array(); + + + foreach($indexes as $index){ + + $title = \gp\tool::IndexToTitle($index); + + // Create file in trash + if( $title ){ + if( !\gp\admin\Content\Trash::MoveToTrash_File($title, $index, $trash_data) ){ + msg($langmessage['OOPS'] . ' (Not Moved)'); + $this->RestoreSettings(); + return false; + } + } + + + // Remove from menu + if( isset($gp_menu[$index]) ){ + + if( count($gp_menu) == 1 ){ + continue; + } + + if( !$this->RmFromMenu($index,false) ){ + msg($langmessage['OOPS']); + $this->RestoreSettings(); + return false; + } + } + + unset($gp_titles[$index]); + unset($gp_index[$title]); + } + + + \gp\admin\Menu\Tools::ResetHomepage(); + + + if( !\gp\admin\Tools::SaveAllConfig() ){ + $this->RestoreSettings(); + return false; + } + + $link = \gp\tool::GetUrl('Admin/Trash'); + msg(sprintf($langmessage['MOVED_TO_TRASH'], $link)); + + + \gp\tool\Plugins::Action('MenuPageTrashed', array($indexes)); + + return true; + } + + + + /** + * Rename + * + */ + public function RenameForm(){ + \gp\Page\Rename::RenameForm(); + } + + public function RenameFile(){ + global $langmessage, $gp_index; + + //prepare variables + $title =& $_REQUEST['title']; + if( !isset($gp_index[$title]) ){ + msg($langmessage['OOPS'] . ' (R0)'); + return false; + } + + \gp\Page\Rename::RenameFile($title); + } + + + + /** + * Toggle Page Visibility + * + */ + public function ToggleVisibility(){ + global $langmessage; + if( isset($_POST['index']) ){ + $_POST += array('visibility' => ''); + \gp\Page\Visibility::Toggle($_POST['index'], $_POST['visibility']); + + if( !isset($_POST['menu']) ){ + + $admin_link = \gp\Page\Edit::ToggleVisibilityLink($_POST['index'], empty($_POST['visibility'])); + $this->page->ajaxReplace[] = array('replace', '.admin-link-toggle-visibility', $admin_link); + + // toggle 'isPrivate' class on element + $this->page->ajaxReplace[] = array('toggle_vis_class', !empty($_POST['visibility']), ''); + } + + \gp\admin\Notifications::UpdateNotifications(); + } + } + + + + /** + * Display a form for selecting the homepage + * + */ + public function HomepageSelect(){ + global $langmessage; + + echo '
        '; + echo '
        '; + + echo '

        '; + echo $langmessage['Homepage']; + echo '

        '; + + echo '

        '; + echo ''; + echo '

        '; + + + echo '

        '; + echo ' '; + echo ''; + echo '

        '; + + echo '
        '; + echo '
        '; + + } + + + + /** + * Set homepage to auto mode + * homepage will be first item in main menu + * + */ + public function HomepageAuto(){ + global $config; + + $config['homepath_auto'] = true; + if( !\gp\admin\Tools::SaveConfig(true) ){ + return; + } + + //update the display + ob_start(); + $this->HomepageDisplay(); + $content = ob_get_clean(); + + $this->page->ajaxReplace[] = array('inner', '.homepage_setting', $content); + + } + + + /** + * Save the posted page as the homepage + * + */ + public function HomepageSave(){ + global $langmessage, $config, $gp_index, $gp_titles; + + $homepage = $_POST['homepage']; + $homepage_key = false; + if( isset($gp_index[$homepage]) ){ + $homepage_key = $gp_index[$homepage]; + }else{ + + foreach($gp_titles as $index => $title){ + if( $title['label'] === $homepage ){ + $homepage_key = $index; + break; + } + } + + if( !$homepage_key ){ + msg($langmessage['OOPS']); + return; + } + } + + $config['homepath_key'] = $homepage_key; + $config['homepath'] = \gp\tool::IndexToTitle($config['homepath_key']); + // custom homepage from post --> disable auto mode + $config['homepath_auto'] = false; + if( !\gp\admin\Tools::SaveConfig(true) ){ + return; + } + + //update the display + ob_start(); + $this->HomepageDisplay(); + $content = ob_get_clean(); + + $this->page->ajaxReplace[] = array('inner', '.homepage_setting', $content); + } + + +} diff --git a/include/admin/Menu/Menus.php b/include/admin/Menu/Menus.php new file mode 100644 index 0000000..1fbc036 --- /dev/null +++ b/include/admin/Menu/Menus.php @@ -0,0 +1,246 @@ +MenuRemove(); + $this->Redirect(); + break; + + + //rename + case 'MenuRenamePrompt': + $this->MenuRenamePrompt(); + return; + + case 'MenuRename': + $this->MenuRename(); + $this->Redirect(); + break; + + + //new + case 'NewMenuPrompt': + $this->NewMenuPrompt(); + return; + + case 'NewMenuCreate': + $this->NewMenuCreate(); + $this->Redirect(); + break; + } + + $this->ShowForm(); + } + + function Redirect(){ + $url = \gp\tool::GetUrl('Admin/Menu','',false); + \gp\tool::Redirect($url); + } + + + /** + * Display a form for creating a new menu + * + */ + public function NewMenuPrompt(){ + global $langmessage; + + echo '
        '; + echo '
        '; + + echo '

        '; + echo $langmessage['Add New Menu']; + echo '

        '; + + echo '

        '; + echo $langmessage['label']; + echo '   '; + echo ''; + echo '

        '; + + echo '

        '; + echo ' '; + echo ''; + echo '

        '; + + echo '
        '; + echo '
        '; + + } + + + + /** + * Create an alternate menu + * + */ + public function NewMenuCreate(){ + global $config, $langmessage, $dataDir; + + $menu_name = $this->AltMenu_NewName(); + if( !$menu_name ){ + return; + } + + $new_menu = \gp\admin\Menu\Tools::AltMenu_New(); + + //get next index + $index = 0; + if( isset($config['menus']) && is_array($config['menus']) ){ + foreach($config['menus'] as $id => $label){ + $id = substr($id,1); + $index = max($index,$id); + } + } + $index++; + $id = 'm'.$index; + + $menu_file = $dataDir.'/data/_menus/'.$id.'.php'; + if( !\gp\tool\Files::SaveData($menu_file,'menu',$new_menu) ){ + msg($langmessage['OOPS'].' (Menu Not Saved)'); + return false; + } + + $config['menus'][$id] = $menu_name; + if( \gp\admin\Tools::SaveConfig(true) ){ + $url = \gp\tool::GetUrl('Admin/Menu','menu='.$id,false); + \gp\tool::Redirect($url); + } + } + + + /** + * Check the posted name of a menu + * + */ + public function AltMenu_NewName(){ + global $langmessage; + + $menu_name = \gp\tool\Editing::CleanTitle($_POST['menu_name'],' '); + if( empty($menu_name) ){ + msg($langmessage['OOPS'].' (Empty Name)'); + return false; + } + + if( array_search($menu_name,$this->avail_menus) !== false ){ + msg($langmessage['OOPS'].' (Name Exists)'); + return false; + } + + return $menu_name; + } + + + /** + * Display a form for editing the name of an alternate menu + * + */ + public function MenuRenamePrompt(){ + global $langmessage; + + $menu_id =& $_GET['id']; + + if( !\gp\admin\Menu\Tools::IsAltMenu($menu_id) ){ + echo '
        '; + echo $langmessage['OOPS']; + echo '
        '; + return; + } + + $menu_name = $this->avail_menus[$menu_id]; + + echo '
        '; + echo '
        '; + echo ''; + echo ''; + + echo '

        '; + echo $langmessage['rename']; + echo '

        '; + + echo '

        '; + echo $langmessage['label']; + echo '   '; + echo ''; + echo '

        '; + + + echo '

        '; + echo ' '; + echo ' '; + echo '

        '; + + echo '
        '; + echo '
        '; + + } + + + + /** + * Rename a menu + * + */ + protected function MenuRename(){ + global $langmessage,$config; + + $menu_id =& $_POST['id']; + + if( !\gp\admin\Menu\Tools::IsAltMenu($menu_id) ){ + msg($langmessage['OOPS']); + return; + } + + $menu_name = $this->AltMenu_NewName(); + if( !$menu_name ){ + return; + } + + $config['menus'][$menu_id] = $menu_name; + if( \gp\admin\Tools::SaveConfig(true) ){ + $this->avail_menus[$menu_id] = $menu_name; + } + } + + + + /** + * Remove an alternate menu from the configuration and delete the data file + * + */ + public function MenuRemove(){ + global $langmessage,$config,$dataDir; + + $menu_id =& $_POST['id']; + if( !\gp\admin\Menu\Tools::IsAltMenu($menu_id) ){ + msg($langmessage['OOPS']); + return; + } + + unset($config['menus'][$menu_id]); + unset($this->avail_menus[$menu_id]); + + \gp\admin\Tools::SaveConfig(true,true); + + + //delete menu file + $menu_file = $dataDir.'/data/_menus/'.$menu_id.'.php'; + if( \gp\tool\Files::Exists($menu_file) ){ + unlink($menu_file); + } + } + + +} \ No newline at end of file diff --git a/include/admin/Menu/Tools.php b/include/admin/Menu/Tools.php new file mode 100644 index 0000000..84453db --- /dev/null +++ b/include/admin/Menu/Tools.php @@ -0,0 +1,406 @@ + $index ){ + if( !isset($gp_menu[$index]) ){ + $avail[$index] = $title; + } + } + return $avail; + } + + /** + * Return array with info about inherited layouts and number of children for all pages in $gp_menu + * + */ + public static function Inheritance_Info(){ + global $gp_menu, $gp_titles; + + $current_par_info = array(); + $prev_level = 0; + $inherit_info = array(); + + + foreach($gp_menu as $id => $titleInfo){ + + $level = $titleInfo['level']; + $inherit_info[$id] = array(); + + //no longer parents + if( $prev_level >= $level ){ + + $temp_level = $prev_level; + while( $temp_level >= $level ){ + unset($current_par_info[$temp_level]); + $temp_level--; + } + } + + foreach($current_par_info as $parent_level => $parent_info){ + $parent_id = $parent_info['id']; + + if( $parent_level < $level ){ + if( isset($inherit_info[$parent_id]['children']) ){ + $inherit_info[$parent_id]['children']++; + }else{ + $inherit_info[$parent_id]['children'] = 1; + } + } + + if( isset($parent_info['gpLayout']) ){ + $inherit_info[$id]['parent_layout'] = $parent_info['gpLayout']; + } + } + + $array = array(); + $array['id'] = $id; + if( isset($gp_titles[$id]['gpLayout']) ){ + $array['gpLayout'] = $gp_titles[$id]['gpLayout']; + } + + $current_par_info[$level] = $array; + + $prev_level = $level; + } + + return $inherit_info; + } + + + /** + * Get the current layout setting for the page give by it's $index + * + */ + public static function CurrentLayout($index){ + global $config, $gp_titles,$gpLayouts; + static $Inherit_Info; + + if( is_null($Inherit_Info) ){ + $Inherit_Info = self::Inheritance_Info(); + } + + if( isset($gp_titles[$index]['gpLayout']) ){ + $layout = $gp_titles[$index]['gpLayout']; + if( isset($gpLayouts[$layout]) ){ + return $layout; + } + } + + if( isset($Inherit_Info[$index]['parent_layout']) ){ + $layout = $Inherit_Info[$index]['parent_layout']; + if( isset($gpLayouts[$layout]) ){ + return $layout; + } + } + + return $config['gpLayout']; + } + + + /** + * Get the css class representing the current page's visibility + * + */ + public static function VisibilityClass($class, $index){ + global $gp_menu, $gp_titles; + + if( isset($gp_titles[$index]['vis']) ){ + $class .= ' private-list'; + return $class; + } + + $parents = \gp\tool::Parents($index,$gp_menu); + foreach($parents as $parent_index){ + if( isset($gp_titles[$parent_index]['vis']) ){ + $class .= ' private-inherited'; + break; + } + } + + return $class; + } + + + /** + * Output Sortable Menu Link and data about the title or external link + * + */ + public static function MenuLink($data, $class = ''){ + + $class = 'gp_label sort '.$class; + $json = \gp\tool::JsonEncode($data); + + echo ''; + } + + + /** + * Make sure the homepage has a value + * + */ + public static function ResetHomepage(){ + global $config, $gp_menu, $gp_titles; + + if( !isset($gp_titles[$config['homepath_key']]) || !empty($config['homepath_auto']) ){ + if( !isset($gp_titles[$config['homepath_key']]) ){ + // former homepage doesn't exist anymore --> enable auto mode + $config['homepath_auto'] = true; + } + reset($gp_menu); + $config['homepath_key'] = key($gp_menu); + $config['homepath'] = \gp\tool::IndexToTitle($config['homepath_key']); + return true; + } + + return false; + } + + + /** + * Create a scrollable title list + * + * @param array $list + * @param string $name + * @pearm string $type + * @param bool $index_as_value + */ + public static function ScrollList($list, $name = 'from_title', $type = 'radio', $index_as_value = false ){ + global $langmessage; + + $list_out = array(); + foreach($list as $title => $index){ + ob_start(); + echo ''; + + $list_out[$title] = ob_get_clean(); + } + + uksort($list_out,'strnatcasecmp'); + echo '
        '; + echo ''; + echo implode('',$list_out); + echo '
        '; + } + + + + /** + * Create a scrollable list of Extra Content Areaas + * @param array $list + * + */ + public static function ScrollListExtra($list){ + global $langmessage; + + $list_out = array(); + foreach($list as $slug => $label){ + ob_start(); + echo ''; + + $list_out[$slug] = ob_get_clean(); + } + + uksort($list_out,'strnatcasecmp'); + echo '
        '; + echo ''; + echo implode('',$list_out); + echo '
        '; + } + + + + /** + * Create a new page from a user post + * + */ + public static function CreateNew(){ + global $gp_index, $gp_titles, $langmessage, $gpAdmin; + + + //check title + $title = $_POST['title']; + $title = \gp\admin\Tools::CheckPostedNewPage($title,$message); + if( $title === false ){ + msg($message); + return false; + } + + + //multiple section types + $type = $_POST['content_type']; + + + // multiple wrapped sections + if( strpos($type,'{') === 0 ){ + $combo = json_decode($type,true); + if( $combo ){ + + $combo += array('wrapper_data' => false); + $content = self::GetComboContent($combo['types'], $combo['wrapper_data']); + + + $type = array(); + // borrowed from \gp\Page\Edit::ResetFileTypes() + foreach($content as $section){ + $type[] = $section['type']; + } + $type = array_unique($type); + $type = array_diff($type,array('')); + sort($type); + $type = implode(',',$type); + } + //single section type + }else{ + $content = \gp\tool\Editing::DefaultContent($type, $_POST['title']); + if( $content['content'] === false ){ + return false; + } + } + + + //add to $gp_index first! + $index = \gp\tool::NewFileIndex(); + $gp_index[$title] = $index; + + if( !\gp\tool\Files::NewTitle($title,$content,$type) ){ + msg($langmessage['OOPS'].' (cn1)'); + unset($gp_index[$title]); + return false; + } + + //add to gp_titles + $new_titles = array(); + $new_titles[$index]['label'] = \gp\admin\Tools::PostedLabel($_POST['title']); + $new_titles[$index]['type'] = $type; + $gp_titles += $new_titles; + + + //add to users editing + if( $gpAdmin['editing'] != 'all' ){ + $gpAdmin['editing'] = rtrim($gpAdmin['editing'],',').','.$index.','; + + + $users = \gp\tool\Files::Get('_site/users'); + $users[$gpAdmin['username']]['editing'] = $gpAdmin['editing']; + \gp\tool\Files::SaveData('_site/users','users',$users); + + } + + return $index; + } + + + /** + * Get nested Section Combo content + * + */ + public static function GetComboContent($types, $wrapper_data, $content=array()){ + + // create wrapper section + $section = \gp\tool\Editing::DefaultContent('wrapper_section'); + $section['contains_sections'] = count($types); + if( is_array($wrapper_data) ){ + // Typesetter > 5.0.3: $wrapper_data may be defined as array by plugins + $section = array_merge($section, $wrapper_data); + }else{ + // Typesetter <= 5.0.3: $wrapper_data is a string (wrapper class) + $section['attributes']['class'] .= ' ' . $wrapper_data; + } + $content[] = $section; + + foreach($types as $type){ + if( is_array($type) ){ + $_wrapper_data = isset($type[1]) ? $type[1] : ''; + $content = self::GetComboContent($type[0], $_wrapper_data, $content); + }else{ + $class = \gp\Page\Edit::TypeClass($type); + $section = \gp\tool\Editing::DefaultContent($type); + $section['attributes']['class'] .= ' ' . $class; + $content[] = $section; + } + } + + return $content; + } + + + /** + * Get the level of the first page in a menu + * + */ + public static function GetRootLevel($menu){ + reset($menu); + $info = current($menu); + if( isset($info['level']) ){ + return $info['level']; + } + return 0; + } + + + /** + * Is the menu an alternate menu + * + */ + public static function IsAltMenu($id){ + global $config; + return isset($config['menus'][$id]); + } + + + /** + * Generate menu data with a single file + * + */ + public static function AltMenu_New(){ + global $gp_menu, $gp_titles; + + if( count($gp_menu) ){ + reset($gp_menu); + $first_index = key($gp_menu); + }else{ + reset($gp_titles); + $first_index = key($gp_titles); + } + + $new_menu[$first_index] = array('level'=>0); + return $new_menu; + } + +} diff --git a/include/admin/Notifications.php b/include/admin/Notifications.php new file mode 100644 index 0000000..14ac848 --- /dev/null +++ b/include/admin/Notifications.php @@ -0,0 +1,959 @@ +CheckNotifications(); + $this->ApplyFilters(); + + + // Get active filters from the admin sessio + if( !empty($gpAdmin['notification_filters']) ){ + $this->filters = $gpAdmin['notification_filters']; + + }elseif( isset($gpAdmin['notifications']['filters']) ){ + $this->filters = $gpAdmin['notifications']['filters']; + unset($gpAdmin['notifications']); + } + + } + + + public static function GetSingleton(){ + if( !self::$singleton ){ + new self(); + } + } + + + /** + * Outputs a list of notifications + * To be rendered in a gpabox + * + */ + public function ListNotifications(){ + global $langmessage; + + $this->FilterUserDefined(); + + $this->debug('$notifications = ' . pre($this->notifications)); + + echo '
        '; + + + + $filter_list_by = ''; + if( isset($_REQUEST['type']) ){ + $filter_list_by = rawurldecode($_REQUEST['type']); + $this->Tabs($filter_list_by); + } + + + foreach( $this->notifications as $type => $notification ){ + + + if( empty($filter_list_by) ){ + $title = $this->GetTitle($notification['title']); + echo '

        ' . $title . '

        '; + }elseif( $type != $filter_list_by ){ + continue; + } + + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach( $notification['items'] as $id => $item ){ + + $tr_class = ''; + $link_icon = ''; + $link_title = $langmessage['Hide']; + if( $item['priority'] < 0 ){ + $tr_class = ' class="notification-item-muted"'; + $link_icon = ''; + $link_title = $langmessage['Show']; + } + + echo ''; + echo ''; + echo ''; + + echo ''; + + echo ''; + } + echo '
        ' . $langmessage['Item'] . '' . $langmessage['options'] . '' . $langmessage['Visibility'] . '
        ' . $item['label'] . '' . $item['action'] . ''; + + echo \gp\tool::Link( + 'Admin/Notifications/Manage', + $link_icon, + 'cmd=toggle_priority' + . '&id=' . rawurlencode($id) + . '&type=' . rawurlencode($filter_list_by), + array( + 'title' => $link_title, + 'class' => 'toggle-notification', + 'data-cmd' => 'gpabox', + ) + ); + + echo '
        '; + } + + echo '

        '; + echo ''; + echo '

        '; + + echo '
        '; + } + + + /** + * Tabs + * Output tabs to navigate between notifications + * @param string $filter_list_by tab type to be active + * + */ + public function Tabs($filter_list_by){ + echo '
        '; + foreach( $this->notifications as $type => $notification ){ + + $class = ''; + + if( $filter_list_by && $type == $filter_list_by ){ + $class = 'selected'; + } + + echo \gp\tool::Link( + 'Admin/Notifications', + $this->GetTitle($notification['title']) . ' ('.$notification['count'].')', + 'cmd=ShowNotifications&type=' . rawurlencode($type), + array( + 'title' => $this->GetTitle($notification['title']), + 'class' => $class, + 'style' => 'margin-right:0.5em;', + 'data-cmd' => 'gpabox', + ) + ); + + } + echo '
        '; + } + + + /** + * Notification Title + * @param string $title + * @return string $title translation if available + * + */ + public function GetTitle($title){ + global $langmessage; + + if( isset($langmessage[$title]) ){ + return $langmessage[$title]; + } + return htmlspecialchars($title); + } + + /** + * Manage Notifications + * Set display filters and priority for notification items by $_REQUEST + * + */ + public function ManageNotifications(){ + global $gpAdmin; + + $cmd = \gp\tool::GetCommand(); + + switch( $cmd ){ + case 'toggle_priority': + if( !empty($_REQUEST['id']) ){ + $this->SetFilter($_REQUEST['id'], 'toggle_priority'); + } + break; + + case 'set_priority': + if( !empty($_REQUEST['id']) && + isset($_REQUEST['new_priority']) && + is_numeric($_REQUEST['new_priority']) + ){ + $this->SetFilter($_REQUEST['id'], 'set_priority', $_REQUEST['new_priority']); + } + break; + } + + + $gpAdmin['notification_filters'] = array_filter($this->filters); // Save filters to the admin session + + $this->ListNotifications(); + self::UpdateNotifications(); + } + + + /** + * Set a single filter + * @param string $id of notification item to be modified + * @param string $do filter action + * @param string $val filter value + * + * currently implemented actions: + * 'toggle_priority' will toggle priority between -1 (=muted) and it's initial value + * 'set_priority' will set the priority value to $val + * + */ + public function SetFilter($id, $do, $val=false){ + + // check if id exists in notifications + $id_exists = false; + foreach( $this->notifications as $type => $notification ){ + if( array_key_exists($id,$notification['items']) ){ + $id_exists = true; + break; + } + } + + if( !$id_exists ){ + // notification id no longer exists, purge possible stray filter + if( isset($this->filters[$id]) ){ + unset($this->filters[$id]); + } + return; + } + + switch( $do ){ + + case 'toggle_priority': + if( isset($this->filters[$id]['priority']) ){ + unset($this->filters[$id]['priority']); + }else{ + $this->filters[$id]['priority'] = -1; + } + break; + + case 'set_priority': + $this->filters[$id]['priority'] = (int)$val; + return; + } + + $this->debug( + 'Notifications SetFilter Error: unknown command "' + . htmlspecialchars($do) . '"' + ); + + } + + + + /** + * Apply filters to the notifications array + * Remove inapropriate items for users lacking permissions to deal with them + * Apply user defined (display) filters + * + */ + public function ApplyFilters(){ + global $gpAdmin; + + + // Remove items lacking user permissions and therefore cannot be dealt with anyway + + // deprecated addons + if( !\gp\admin\Tools::HasPermission('Admin/Addons') ){ + $this->FilterType('Deprecated Addons','deprecated_addons'); + } + + // debug / development + if( $gpAdmin['granted'] != 'all' || $gpAdmin['editing'] != 'all' ){ + $this->FilterType('Development','superuser'); + } + + // extra content draft + if( !\gp\admin\Tools::HasPermission('Admin/Extra') ){ + $this->FilterType('Working Drafts','extra'); + } + + // theme update + if( !\gp\admin\Tools::HasPermission('Admin_Theme_Content/Remote') ){ + $this->FilterType('updates','theme'); + } + + // addon update + if( !\gp\admin\Tools::HasPermission('Admin/Addons/Remote') ){ + $this->FilterType('updates','plugin'); + } + + // core update + if( !\gp\admin\Tools::HasPermission('Admin/Uninstall') ){ // can't find a permission for core updates so I use Uninstall + $this->FilterType('updates','core'); + } + + // page draft + $this->FilterCallback('Working Drafts',function($item){ + if( $item['type'] == 'page' && !\gp\admin\Tools::CanEdit($item['title']) ){ + return true; + } + return false; + }); + + + // private page + $this->FilterCallback('Private Pages',function($item){ + if( !\gp\admin\Tools::CanEdit($item['title']) ){ + return true; + } + return false; + }); + + } + + + /** + * Apply user defined (display) filters + * Remove empty, count items, and get priority + * + */ + public function FilterUserDefined(){ + + foreach( $this->notifications as $notification_type => &$notification ){ + + if( empty($this->notifications[$notification_type]['items']) ){ + unset($this->notifications[$notification_type]); + continue; + } + + $count = 0; + $priority = 0; + $total_priority = 0; + foreach( $notification['items'] as $id => &$item ){ + + $total_priority = max( $item['priority'], $total_priority ); + + if( isset($this->filters[$id]) ){ + $item = $this->filters[$id] + $item; + } + + if( $item['priority'] > 0 ){ + $priority = max( $item['priority'], $priority ); + $count++; + } + + } + + $notification['count'] = $count; + $notification['priority'] = $priority; + $notification['total_priority'] = $total_priority; + + } + + // sort by priority + uasort($this->notifications,function($a,$b){ + + if( $b['priority'] !== $a['priority'] ){ + return strnatcmp($b['priority'],$a['priority']); + } + + if( $b['total_priority'] !== $a['total_priority'] ){ + return strnatcmp($b['total_priority'],$a['total_priority']); + } + + return strnatcmp($b['title'],$a['title']); + }); + } + + + + /** + * Filter notifications matching a notification type and item type + * + */ + public function FilterType( $notification_type, $item_type ){ + + if( !isset($this->notifications[$notification_type]) ){ + return; + } + + foreach( $this->notifications[$notification_type]['items'] as $id => $item ){ + + if( $item['type'] !== $item_type ){ + continue; + } + + unset($this->notifications[$notification_type]['items'][$id]); + } + + } + + /** + * Filter notifications matching a notification type with a callback + * + */ + public function FilterCallback( $notification_type, $callback ){ + if( !isset($this->notifications[$notification_type]) ){ + return; + } + foreach( $this->notifications[$notification_type]['items'] as $id => $item ){ + if( $callback($item) === true ){ + unset($this->notifications[$notification_type]['items'][$id]); + } + } + } + + + /** + * Aggregate all sources of notifications + * + */ + public function CheckNotifications(){ + + $this->notifications = array(); + + + $items = $this->GetDrafts(); + $this->Add('Working Drafts', $items, '#329880'); + + + $items = $this->GetPrivatePages(); + $this->Add('Private Pages', $items, '#ad5f45'); + + + $items = $this->GetUpdatesNotifications(); + $this->Add('updates', $items, '#3153b7'); + + + $items = $this->GetDeprecatedAddons(); + $this->Add('Deprecated Addons', $items, '#d61b1b'); + + + $items = $this->GetDebugNotifications(); + $this->Add('Development', $items, '#ff8c00', '#000'); + + + \gp\tool\Plugins::Action('Notifications',[$this]); + + } + + + /** + * Add Notifications + * @param string $title will sho up as panelgroup submenu item + * @param array $items see below + * @param string $bg (optional) badge background color (any valid css color value) + * @param string $color (optional) badge text color (any valid color value), use a dark color on light $bg + * + * $items are associative arrays containing the following keys: + * 'label' => (string) text or html, first element of the item row + * 'id' => (string) arbitrary but permanently unique string or number (required for user defined filtering) + * 'priority' => (integer) a number above 0, determines the ranking amongst other notifications. Anything above 100 is high-priority + * 'action' => (string) plain text or html describing and/or linking to possible solutions + * 'type' => (string) optional, only relevant for automatic system-internal filters + * + */ + public function Add( $title, $items, $bg = '#555', $color = '#fff'){ + + if( empty($items) ){ + return; + } + + if( !isset($this->notifications[$title]) ){ + $this->notifications[$title] = [ + 'title' => $title, + 'badge_bg' => $bg, + 'badge_color' => $color, + 'items' => [], + ]; + } + + foreach( $items as $item ){ + + if( !isset($item['id']) ){ + trigger_error('id not set for notification '.pre($item)); // should we create an id? + continue; + } + + $item += ['priority'=>0]; + $item['priority'] = (int)$item['priority']; + $id = hash('crc32b', $item['id']); + + unset($item['id']); + + $this->notifications[$title]['items'][$id] = $item; + } + + + } + + + /** + * Get Notifications + * Outputs a Notifications panelgroup + * @param boolean $in_panel if panelgroup shall be rendered in admin menu + * + */ + public function GetNotifications($in_panel=true){ + global $langmessage; + + $this->FilterUserDefined(); + + if( count($this->notifications) < 1 ){ + return; + } + + + $total_count = 0; + $main_badge_style = ''; + $expand_class = ''; // expand_child_click + $badge_format = ' (%2$d)
        '; + $panel_class = ''; + $default_style = ['badge_bg'=>'transparent','color'=>'#fff']; + + if( $in_panel ){ + $badge_format = ' %2$d'; + $expand_class = 'expand_child'; + $panel_class = 'admin-panel-notifications'; + } + + + + ob_start(); + foreach($this->notifications as $type => $notification ){ + + if( empty($notification['items']) ){ + $this->debug('notification => items subarray mising or empty'); + continue; + } + + + $total_count += $notification['count']; + $title = $this->GetTitle($notification['title']); + $badge_html = ''; + $badge_style = ''; + $notification += $default_style; + + + if( $notification['count'] > 0 ){ + $badge_style = 'background-color:'.$notification['badge_bg'].';color:'.$notification['badge_color'].';'; + $badge_html = sprintf($badge_format, $badge_style, $notification['count']); + } + + + echo '
      • '; + echo \gp\tool::Link( + 'Admin/Notifications', + $title . $badge_html, + 'cmd=ShowNotifications&type=' . rawurlencode($type), + array( + 'title' => $notification['count'] . ' ' . $title, + 'class' => 'admin-panel-notification', // . '-' . rawurlencode($type)', + 'data-cmd' => 'gpabox', + ) + ); + echo '
      • '; + + if( empty($main_badge_style) ){ + $main_badge_style = $badge_style; + } + } + + $links = ob_get_clean(); + + $panel_label = $langmessage['Notifications']; + + $badge_html = $total_count > 0 ? + '' . $total_count . '' : + ''; + + \gp\Admin\Tools::_AdminPanelLinks( + $in_panel, + $links, + $panel_label, + 'fa fa-bell', + 'notifications', + $panel_class, // new param 'class' + $badge_html // new param 'badge' + ); + + } + + + /** + * Get a Notification array for deprecatd addons as defined in /gpconfig.php + * @return array $deprecated_note single notification array + * + */ + public function GetDeprecatedAddons(){ + global $config, $deprecated_addons, $langmessage; + + $deprecated_note = array(); + + if( !\notify_deprecated || !is_array($config['addons']) || empty($deprecated_addons) ){ + return $deprecated_note; + } + + foreach($deprecated_addons as $deprec_addon_name => $deprec_addon_data){ + + if( empty($deprec_addon_data['notify']) ){ + continue; + } + + foreach( $config['addons'] as $inst_addon_key => $inst_addon_data ){ + + if( $deprec_addon_name != $inst_addon_data['name'] ){ + // addon is not installed + continue; + } + + $version_affected = empty($deprec_addon_data['upto_version']) || + $deprec_addon_data['upto_version'] == 'all' || + version_compare($deprec_addon_data['upto_version'], $inst_addon_data['version'], '<='); + if( !$version_affected ){ + continue; + } + + $label = $deprec_addon_name; + $uninstall_link = \gp\tool::Link( + 'Admin/Addons', + $langmessage['uninstall'], + 'cmd=uninstall&addon=' . $inst_addon_data['data_folder'], + array( + 'data-cmd' => 'gpabox', + 'title' => $langmessage['uninstall'] + ) + ); + $action = empty($deprec_addon_data['reason']) ? + $uninstall_link : + $deprec_addon_data['reason'] . ' ' . $uninstall_link; + + $deprecated_note[] = array( + 'type' => 'deprecated_addons', + 'label' => $label, + 'id' => 'deprecated addon ' . $label, + 'priority' => 200, // that's a rather high priority + 'action' => $action, + ); + + } + } + + return $deprecated_note; + } + + + /** + * Get a Notification array for debugging / development relevant information + * @return array $debug_note single notification array + * + */ + public function GetDebugNotifications(){ + global $langmessage; + + $debug_note = array(); + + if( ini_get('display_errors') ){ + $label = 'ini_set(display_errors,' . htmlspecialchars(ini_get('display_errors')) . ')'; + $debug_note[] = array( + 'type' => 'any_user', + 'label' => $label, + + // Adding the server name to the id makes sure that it will change when moving the site (e.g. when going live) + // Thus possible set hide-filters will invalidate and the warning will show up again. + 'id' => $label . \gp\tool::ServerName(), + + 'priority' => 500, // that's a high priority + 'action' => 'edit gpconfig.php or notify administrator!
        This should only be enabled in exceptional cases.', + ); + } + + if( \gpdebug ){ + $label = 'gpdebug is enabled'; + $debug_note[] = array( + 'type' => 'superuser', + 'label' => $label, + 'id' => $label . \gp\tool::ServerName(), + 'priority' => 75, + 'action' => 'edit gpconfig.php', + ); + } + + if( \create_css_sourcemaps ){ + $label = 'create_css_sourcemaps is enabled'; + $debug_note[] = array( + 'type' => 'superuser', + 'label' => $label, + 'id' => $label . \gp\tool::ServerName(), + 'priority' => 75, + 'action' => 'edit gpconfig.php', + ); + } + + return $debug_note; + } + + + + /** + * Convert $new_versions to notification array + * @return array $updated single notification array containing possible updates + * + */ + public function GetUpdatesNotifications(){ + global $langmessage; + + $updates = array(); + + if( gp_remote_update && isset(\gp\Admin\Tools::$new_versions['core']) ){ + $label = \CMS_NAME . ' ' . \gp\Admin\Tools::$new_versions['core']; + $updates[] = array( + 'type' => 'core', + 'label' => $label, + 'id' => $label, + 'priority' => 60, + 'action' => '
        ' + . $langmessage['upgrade'] . '', + ); + } + + foreach(\gp\Admin\Tools::$new_versions as $addon_id => $new_addon_info){ + + if( !is_numeric($addon_id) ){ + continue; + } + + $label = $new_addon_info['name'] . ': ' . $new_addon_info['version']; + $url = \gp\admin\Tools::RemoteUrl( $new_addon_info['type'] ); + + if( $url === false ){ + continue; + } + $updates[] = array( + 'type' => $new_addon_info['type'], + 'label' => $label, + 'id' => $label, + 'priority' => 60, + 'action' => '' + . $langmessage['upgrade'] . '', + ); + + } + + return $updates; + } + + + + /** + * Update the Notifications panelgroup in Admin Menu via AJAX + * + */ + public static function UpdateNotifications(){ + global $page; + + + if( !\gp\admin\Tools::HasPermission('Admin/Notifications') ){ + return; + } + + + self::GetSingleton(); + + ob_start(); + self::$singleton->GetNotifications(); + $panelgroup = ob_get_clean(); + + + $page->ajaxReplace[] = array('replace', '.admin-panel-notifications', $panelgroup); + + } + + + /** + * Get information about working drafts + * @return array $drafts single notification array of current working drafts + * + */ + public function GetDrafts(){ + global $dataDir, $gp_index, $gp_titles, $langmessage; + + $draft_types = array( + 'page' => $dataDir . '/data/_pages', + 'extra' => $dataDir . '/data/_extra', + ); + + $drafts = array(); + + foreach( $draft_types as $type => $dir ){ + + $folders = \gp\tool\Files::readDir($dir,1); + + foreach( $folders as $folder ){ + + // no draft + $draft_path = $dir . '/' . $folder . '/draft.php'; + if( !\gp\tool\Files::Exists($draft_path) ){ + continue; + } + + // drafts of 'trashed' pages will not count + $deleted_path = $dir . '/' . $folder . '/deleted.php'; + if( \gp\tool\Files::Exists($deleted_path) ){ + continue; + } + + $draft = array( + 'type' => $type, + 'id' => $folder, + 'priority' => 100, + ); + + switch( $type ){ + case 'extra': + $draft['label'] = str_replace('_', ' ', $folder) . ' (' . $langmessage['theme_content'] . ')'; + $draft['action'] = \gp\tool::Link( + 'Admin/Extra', + $langmessage['theme_content'], + '', + array( + 'class' => 'getdrafts-extra-content-link', + 'title' => $langmessage['theme_content'], + ) + ); + $draft['preview_link'] = \gp\tool::Link( + 'Admin/Extra', + $langmessage['preview'], + 'cmd=PreviewText&file=' . $folder, + array( + 'class' => 'getdrafts-extra-preview', + 'title' => $langmessage['preview'], + ) + ); + $draft['publish_link'] = \gp\tool::Link( + 'Admin/Extra', + $langmessage['Publish Draft'], + 'cmd=PublishDraft&file=' . $folder, + array( + 'data-cmd' => 'gpajax', + 'class' => 'getdrafts-extra-publish', + 'title' => $langmessage['Publish Draft'], + ) + ); + $draft['folder'] = $folder; + break; + + case 'page': + $draft['index'] = substr($folder, strpos($folder, "_") + 1); + $draft['title'] = \gp\tool::IndexToTitle($draft['index']); + $draft['label'] = \gp\tool::GetLabel($draft['title']) . ' (' . $langmessage['Page'] . ')'; + $draft['action'] = \gp\tool::Link( + $draft['title'], + $langmessage['view/edit_page'], //$draft['label'], + '', + array( + 'class' => 'getdrafts-page-link', + 'title' => $langmessage['view/edit_page'], + ) + ); + $draft['publish_link'] = \gp\tool::Link( + $draft['title'], + $langmessage['Publish Draft'], + 'cmd=PublishDraft', + array( + 'data-cmd' => 'creq', + 'class' => 'getdrafts-page-publish', + 'title' => $langmessage['Publish Draft'], + ) + ); + break; + } + + $drafts[] = $draft; + + } + + } + + return $drafts; + } + + + /** + * Get information of all private (invisible) pages + * @return array $private_pages single notification array of pages currently set as private + * + */ + public function GetPrivatePages(){ + global $gp_titles, $langmessage, $page; + + $private_pages = array(); + foreach( $gp_titles as $index => $title ){ + + if( !isset($title['vis']) || $title['vis'] !== 'private' ){ + continue; + } + + $private_page = array( + 'index' => $index, + 'title' => \gp\tool::IndexToTitle($index), + 'id' => 'private_page' . $index, + 'priority' => 40, + 'label' => \gp\tool::GetLabelIndex($index), + ); + + // increase priority by 100 when viewing the current page + if( isset($page->gp_index) && $page->gp_index == $index ){ + $private_page['priority'] += 100; + } + + $private_page['action'] = \gp\tool::Link( + $private_page['title'], + $langmessage['view/edit_page'], + '', + array( + 'class' => 'getprivate-page-link', + 'title' => $langmessage['view/edit_page'], + ) + ); + $private_page['make_public_link'] = \gp\tool::Link( + 'Admin/Menu/Ajax', + $langmessage['Visibility'] . ' ' . $langmessage['Public'], + 'cmd=ToggleVisibility&index=' . $index, + array( + 'data-cmd' => 'postlink', + 'class' => 'getprivate-make-public', + 'title' => $langmessage['Publish Draft'], + ) + ); + $private_pages[] = $private_page; + + } + + return $private_pages; + } + + + public function debug($msg){ + $this->debug && debug($msg); + } + + + } +} diff --git a/include/admin/Page.php b/include/admin/Page.php new file mode 100644 index 0000000..e8fe942 --- /dev/null +++ b/include/admin/Page.php @@ -0,0 +1,404 @@ +requested = str_replace(' ','_',$title); + $this->label = $langmessage['administration']; + $this->scripts = \gp\admin\Tools::AdminScripts(); + $this->script_keys = array_keys($this->scripts); + $this->script_keys = array_combine( str_replace('_','/',$this->script_keys), $this->script_keys); + $this->lang = $config['language']; + $this->language = $languages[$this->lang]; + + $this->head .= "\n".''; + @header( 'X-Frame-Options: SAMEORIGIN' ); + } + + + public function RunScript(){ + + ob_start(); + $this->RunAdminScript(); + $this->contentBuffer = ob_get_clean(); + + + //display admin area in full window? + if( $this->FullDisplay() ){ + $this->get_theme_css = false; + $_REQUEST['gpreq'] = 'admin'; + } + } + + //display admin area in full window + private function FullDisplay(){ + + if( \gp\tool::RequestType() == 'template' + && $this->show_admin_content + ){ + return true; + } + + return false; + } + + + //called by templates + public function GetContent(){ + + $this->GetGpxContent(); + + /* rendering non_admin_content moved to GetGpxContent() */ + + echo '
        '; + \gp\tool\Output::Get('AfterContent'); + \gp\tool\Plugins::Action('GetContent_After'); + echo '
        '; + } + + + public function GetGpxContent($ajax = false){ + global $gp_admin_html; + + if( empty($this->show_admin_content) ){ + /* non_admin_content inside #gpx_content */ + echo '
        '; + $this->GetNonAdminContent(); + echo '
        '; + return; + } + + $request_type = \gp\tool::RequestType(); + if( $request_type == 'body' ){ + echo $this->contentBuffer; + return; + } + + ob_start(); + echo '
        '; + echo '
        '; + $this->AdminContentPanel(); + $this->BreadCrumbs(); + echo '
        '; + echo $this->contentBuffer; + echo '
        '; + echo '
        '; // /#admincontent + + /* non_admin_content is now inside #gpx_content */ + $this->GetNonAdminContent(); + + echo '
        '; // /#gpx_content + $admin_content = ob_get_clean(); + + if( !$ajax ){ + $gp_admin_html .= '
        '.$admin_content.'
        '; + return; + } + echo $admin_content; + } + + + public function GetNonAdminContent(){ + if( !empty($this->non_admin_content) ){ + echo '
        '; + if( strpos(\gp\tool\Output::$components, 'bootstrap') !== false ){ + echo $this->non_admin_content_bootstrap; + }else{ + echo $this->non_admin_content; + } + echo '
        '; + echo '
        '; + } + } + + + private function BreadCrumbs(){ + global $langmessage, $config; + + echo '
        '; + + echo \gp\tool::Link('',$langmessage['Homepage']); + echo ' » '; + echo \gp\tool::Link('Admin',$langmessage['administration']); + + + $crumbs = array(); + $request_string = str_replace('_','/',$this->requested); + $parts = explode('/',$request_string); + + $addon_key = false; + do{ + + $request_string = implode('/',$parts); + $scriptinfo = $this->GetScriptInfo($request_string); + + if( isset($scriptinfo['addon']) ){ + $addon_key = $scriptinfo['addon']; + } + + if( is_array($scriptinfo) && isset($scriptinfo['label']) ){ + $crumbs[$request_string] = $scriptinfo['label']; + } + }while(array_pop($parts)); + + + //add addon to crumbs + if( $addon_key && isset($config['addons'][$addon_key]) ){ + + $slug = 'Admin/Addons/'.\gp\admin\Tools::encode64($addon_key); + $crumbs[$slug] = $config['addons'][$addon_key]['name']; + + $crumbs['Admin/Addons'] = $langmessage['plugins']; + } + + + //page label + $this->label = implode(' « ', $crumbs); + + //add to breadcrumbs + $crumbs = array_reverse($crumbs); + foreach($crumbs as $slug => $label){ + echo ' » '; + echo \gp\tool::Link($slug,$label); + } + + + + echo '
        '; + } + + + /** + * Output toolbar for admin window + * + */ + private function AdminContentPanel(){ + global $langmessage; + + echo '
        '; + echo '
        '; + \gp\tool\Output::GetTopTwoMenu(); + echo '
        '; + + + self::ToolbarSearch(); + + echo '
        '; + } + + + public static function ToolbarSearch(){ + echo ''; + } + + /** + * Find the requested admin script and execute it if the user has permissions to view it + * + */ + private function RunAdminScript(){ + global $dataDir, $langmessage; + + + if( strtolower($this->requested) == 'admin' ){ + $this->AdminPanel(); + return; + } + + + //resolve request for /Admin_Theme_Content if the request is for /Admin_Theme_Conent/1234 + $request_string = str_replace('_','/',$this->requested); + $parts = explode('/',$request_string); + $extra_parts = []; + + + do{ + + $request_string = implode('/',$parts); + $scriptinfo = $this->GetScriptInfo($request_string); + if( is_array($scriptinfo) ){ + + if( \gp\admin\Tools::HasPermission($request_string) ){ + + $this->OrganizeFrequentScripts($request_string); + + // get extra parts without underscores replaced with slashes + $len = strlen($request_string); + $extra = substr($this->requested,$len); + $extra_parts = explode('/',$extra); + $extra_parts = array_filter($extra_parts); + $extra_parts = array_values($extra_parts); + + \gp\tool\Output::ExecInfo($scriptinfo, array('page'=>$this,'path_parts'=>$extra_parts) ); + + return; + } + + msg($langmessage['not_permitted'] . ' (' . $request_string . ')'); + $this->AdminPanel(); + return; + } + + + //these are here because they should be available to everyone + switch($request_string){ + case 'Admin/Finder': + if( \gp\admin\Tools::HasPermission('Admin_Uploaded') ){ + includeFile('thirdparty/elFinder/connector.php'); + return; + } + break; + + } + + array_pop($parts); + + }while( count($parts) ); + + $this->Redirect(); + } + + + /** + * Get admin script info if the request slug uses underscores or slashes + * + */ + private function GetScriptInfo(&$request_string){ + + if( isset($this->script_keys[$request_string]) ){ + $request_string = $this->script_keys[$request_string]; + return $this->scripts[$request_string]; + } + + return false; + } + + + /** + * Redirect admin request to the most similar page + * + */ + private function Redirect(){ + + + //find similar + $scripts = $this->scripts; + $scripts['Admin'] = array(); + $similar = array(); + $lower_req = strtolower($this->requested); + + foreach($scripts as $key => $script_info){ + $lower_key = strtolower($key); + + similar_text($lower_req,$lower_key,$percent); + $similar[$key] = $percent; + } + + arsort($similar); + + $redir_key = key($similar); + $location = \gp\tool::GetUrl($redir_key,'',false); + \gp\tool::Redirect($location); + } + + + /** + * Show the default admin page + * + */ + private function AdminPanel(){ + global $langmessage; + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'embededcheck': + new \gp\admin\Update('embededcheck'); + return; + + case 'autocomplete-titles': + $opts = array('var_name'=>false); + echo \gp\tool\Editing::AutoCompleteValues(false,$opts); + die(); + } + + $this->head_js[] = '/include/js/auto_width.js'; + + echo '

        '.$langmessage['administration'].'

        '; + + echo '
        '; + \gp\admin\Tools::AdminPanelLinks(false); + echo '
        '; + } + + + /** + * Increment freq_scripts for $page and sort by counts + * + */ + private function OrganizeFrequentScripts($page){ + global $gpAdmin; + + if( !isset($gpAdmin['freq_scripts']) ){ + $gpAdmin['freq_scripts'] = array(); + } + if( !isset($gpAdmin['freq_scripts'][$page]) ){ + $gpAdmin['freq_scripts'][$page] = 0; + }else{ + $gpAdmin['freq_scripts'][$page]++; + if( $gpAdmin['freq_scripts'][$page] >= 10 ){ + $this->CleanFrequentScripts(); + } + } + + arsort($gpAdmin['freq_scripts']); + } + + + /** + * Reduce the number of scripts in freq_scripts + * + */ + private function CleanFrequentScripts(){ + global $gpAdmin; + + //reduce to length of 5; + $count = count($gpAdmin['freq_scripts']); + if( $count > 3 ){ + for($i=0;$i < ($count - 5);$i++){ + array_pop($gpAdmin['freq_scripts']); + } + } + + //reduce the hit count on each of the top five + $min_value = end($gpAdmin['freq_scripts']); + foreach($gpAdmin['freq_scripts'] as $page => $hits){ + $gpAdmin['freq_scripts'][$page] = $hits - $min_value; + } + } + + +} diff --git a/include/admin/Settings/CKEditor.php b/include/admin/Settings/CKEditor.php new file mode 100644 index 0000000..2e19335 --- /dev/null +++ b/include/admin/Settings/CKEditor.php @@ -0,0 +1,563 @@ +page->css_admin[] = '/include/css/addons.css'; + //$this->page->head_js[] = '/include/js/admin_ckeditor.js'; + + $this->Init(); + + // subpage + $this->subpages = array( + '' => $langmessage['Manage Plugins'], + 'Config' => $langmessage['configuration'], + 'Example' => 'Example', + ); + + + $parts = explode('/',$this->page->requested); + + if( count($parts) > 2 && array_key_exists( $parts[2], $this->subpages ) ){ + $this->current_subpage = $parts[2]; + } + + + // commands + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + + case 'save_custom_config': + $this->SaveCustomConfig(); + break; + + case 'upload_plugin': + $this->UploadPlugin(); + break; + + case 'rmplugin': + $this->RemovePlugin(); + break; + } + + echo ''; + + $this->Heading(); + + + switch($this->current_subpage){ + case 'Example': + $this->Example(); + break; + case 'Config': + $this->CustomConfigForm(); + $this->DisplayCurrent(); + break; + default: + $this->PluginForm(); + break; + } + + echo '

        '; + echo 'CKEditor is '.CMS_NAME.'\'s text editor of choice because it is a powerful tool with many configuration options and a growing list of plugins.'; + echo '

        '; + } + + function Heading(){ + + echo '

        CKEditor » '; + + $separator = ''; + foreach($this->subpages as $slug => $label){ + echo $separator; + if( $slug == $this->current_subpage ){ + echo $label; + }else{ + echo \gp\tool::Link( rtrim('Admin/CKEditor/'.$slug,'/'), $label ); + } + $separator = ' | '; + } + echo '

        '; + + } + + + /** + * Display a form for uploading CKEditor plugins + * + */ + function PluginForm(){ + global $langmessage; + + echo '
        '; + echo ''; + if( count($this->cke_config['plugins']) ){ + foreach($this->cke_config['plugins'] as $plugin_name => $plugin_info){ + echo ''; + + } + } + + echo ''; + + echo '
        '.$langmessage['name'].''.$langmessage['Modified'].''.$langmessage['options'].'
        '; + echo $plugin_name; + echo ''; + echo \gp\tool::date($langmessage['strftime_datetime'],$plugin_info['updated']); + echo ''; + + $attr = array('data-cmd'=>'postlink', 'class'=>'gpconfirm','title'=>sprintf($langmessage['generic_delete_confirm'],$plugin_name)); + echo \gp\tool::Link($this->page->requested,$langmessage['delete'],'cmd=rmplugin&plugin='.rawurlencode($plugin_name), $attr ); + echo '
        '; + echo ''; + echo ''; + echo ' '; + echo ''; + echo ' '; + echo '
        '; + echo '
        '; + + + //$this->build_config + if( $this->build_config && isset($this->build_config['plugins']) ){ + + $ordered = array(); + $count = 0; + foreach($this->build_config['plugins'] as $plugin => $status){ + if( !$status ){ + continue; + } + + $char = strtoupper($plugin[0]); + $ordered[$char][] = ''.ucfirst($plugin).''; + $count++; + } + + //echo '

        '.$langmessage['Installed'].'

        '; + echo '


        '; + echo ''; + echo ''; + foreach($ordered as $char => $plugins){ + echo ''; + } + echo '
        '.$langmessage['Installed'].' ('.$count.')
        '; + echo ''.$char.''; + echo ''; + echo implode(', ',$plugins); + echo '
        '; + } + + } + + + /** + * Add an uploaded plugin + * + */ + function UploadPlugin(){ + global $langmessage, $dataDir; + + $archive = $this->UploadedArchive(); + if( !$archive ){ + return false; + } + + + + // get plugin name and check file types + $list = $archive->ListFiles(); + $plugin_name = ''; + $remove_path = ''; + + foreach($list as $file){ + + //don't check extensions on folder + if( $file['size'] == 0 ){ + continue; + } + + //check extension + if( !\gp\admin\Content\Uploaded::AllowedExtension($file['name'], false) ){ + msg($langmessage['OOPS'].' (File type not allowed:'.htmlspecialchars($file['name']).')'); + return false; + } + + + //plugin name + if( strpos($file['name'],'plugin.js') !== false ){ + + $new_plugin_name = $this->FindPluginName($archive, $file['name']); + if( !$new_plugin_name ){ + continue; + } + + //use the most relevant plugin name + $new_path = dirname($file['name']); + if( !$plugin_name || strlen($new_path) < strlen($remove_path) ){ + $plugin_name = $new_plugin_name; + $remove_path = $new_path; + } + } + } + + + if( !$this->CanUpload($plugin_name) ){ + return; + } + + + //extract to temporary location + $extract_temp = $dataDir.\gp\tool\FileSystem::TempFile('/data/_temp/'.$plugin_name); + if( !$archive->extractTo($extract_temp) ){ + \gp\tool\Files::RmAll($extract_temp); + msg($langmessage['OOPS'].' (Couldn\'t extract to temp location)'); + return false; + } + + + //move to _ckeditor folder + $destination = $dataDir.'/data/_ckeditor/'.$plugin_name; + $rename_from = $extract_temp.'/'.ltrim($remove_path,'/'); + if( !\gp\tool\Files::Replace($rename_from, $destination) ){ + msg($langmessage['OOPS'].' (Not replaced)'); + return false; + } + + + // save configuration + if( !array_key_exists( $plugin_name, $this->cke_config['plugins'] ) ){ + $this->cke_config['plugins'][$plugin_name] = array('installed'=>time()); + } + + $this->cke_config['plugins'][$plugin_name]['updated'] = time(); + $this->SaveConfig(); + + msg($langmessage['SAVED']); + } + + + /** + * Get an archive object from the uploaded file + * + */ + function UploadedArchive(){ + global $langmessage; + + if( empty($_FILES['plugin']) ){ + msg($langmessage['OOPS'].' (No File)'); + return; + } + + $plugin_file = $_FILES['plugin']; + + if( strpos($plugin_file['name'],'.zip') === false ){ + msg($langmessage['OOPS'].' (Not a zip file)'); + return; + } + + //rename tmp file to have zip extenstion + if( !\gp\tool\Files::Rename($plugin_file['tmp_name'], $plugin_file['tmp_name'].'.zip') ){ + msg($langmessage['OOPS'].' (Not renamed)'); + return; + } + + $plugin_file['tmp_name'] .= '.zip'; + + + return new \gp\tool\Archive($plugin_file['tmp_name']); + } + + + /** + * Determine if we can upload the plugin + * @param string $plugin_name + * @return bool + */ + function CanUpload($plugin_name){ + global $langmessage; + + if( empty($plugin_name) ){ + msg($langmessage['OOPS'].' (Unknown plugin name)'); + return false; + } + + + //make sure plugin name isn't already in build_config + if( $this->build_config + && isset($this->build_config['plugins']) + && isset($this->build_config['plugins'][$plugin_name]) + && $this->build_config['plugins'][$plugin_name] > 0 ){ + msg($langmessage['addon_key_defined'], ''.$plugin_name.''); + return false; + } + + return true; + } + + + /** + * Get the plugin name from the plugin.js file + * use regular expression to look for "CKEDITOR.plugins.add('plugin-name'" + */ + function FindPluginName($archive, $name){ + + $content = $archive->getFromName($name); + + if( !$content ){ + return false; + } + + + $pattern = '/CKEDITOR\s*\.\s*plugins\s*\.\s*add\s*\(\s*[\'"]([^\'"]+)[\'"]/'; + + if( !preg_match($pattern,$content,$match) ){ + return false; + } + + return $match[1]; + } + + + /** + * Remove a plugin + * + */ + function RemovePlugin(){ + global $langmessage, $dataDir; + + $plugin =& $_REQUEST['plugin']; + if( !is_array($this->cke_config['plugins']) || !array_key_exists( $plugin, $this->cke_config['plugins'] ) ){ + msg($langmessage['OOPS'].' ( )'); + return; + } + + unset( $this->cke_config['plugins'][$plugin] ); + if( !$this->SaveConfig() ){ + msg($langmessage['OOPS'].' (Not Saved)'); + }else{ + msg($langmessage['SAVED']); + } + + + $path = $dataDir.'/data/_ckeditor/'.$plugin; + \gp\tool\Files::RmAll( $path ); + } + + + /** + * Display custom_config form + * + */ + function CustomConfigForm(){ + echo '
        '; + + $placeholder = '{ "example_key": "example_value" }'; + echo '

        '; + echo ''; + echo '

        '; + + + echo '

        '; + echo ''; + echo ''; + echo '

        '; + + echo '
        '; + } + + /** + * Save custom_config value + * + */ + function SaveCustomConfig(){ + global $langmessage; + + $custom_config =& $_REQUEST['custom_config']; + $decoded = array(); + if( !empty($custom_config) ){ + $decoded = json_decode($custom_config,true); + if( !is_array($decoded) ){ + msg($langmessage['OOPS'].' (Invalid JSON String)'); + return false; + } + } + + $this->cke_config['custom_config'] = $decoded; + + if( !$this->SaveConfig() ){ + msg($langmessage['OOPS'].' (Not Saved)'); + }else{ + msg($langmessage['SAVED']); + } + + } + + + /** + * Show a CKEditor instance + * + */ + function Example(){ + + $content = '

        Lorem Ipsum

        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tempor lectus id lectus laoreet scelerisque.

        Vestibulum suscipit, lectus a feugiat facilisis, enim arcu fringilla nisi, et scelerisque nibh sapien in quam. Vivamus sit amet elementum nibh. Donec id ipsum nibh. Aliquam ligula nulla, condimentum sit amet consectetur eu, sagittis id ligula. In felis justo, feugiat et luctus sit amet, feugiat eget odio. Nullam suscipit mollis ipsum nec ultrices. Praesent ut lacus lorem. Fusce adipiscing arcu vitae dui ullamcorper a imperdiet felis dignissim. Maecenas eget tortor mi.

        '; + \gp\tool\Editing::UseCK($content); + } + + + /** + * Display Current Configuration Settings + * + */ + function DisplayCurrent(){ + global $langmessage; + + echo '

        '.$langmessage['Current Configuration'].'

        '; + + $default_config = \gp\tool\Editing::CKConfig(array(),'array'); + echo '
        ';
        +		echo self::ReadableJson($default_config);
        +		echo '
        '; + } + + + /** + * Save the configuration file + * + */ + function SaveConfig(){ + return \gp\tool\Files::SaveData($this->config_file,'cke_config',$this->cke_config); + } + + + /** + * Get current configuration settings + * + */ + function Init(){ + + $this->config_file = '_ckeditor/config'; + $this->cke_config = \gp\tool\Files::Get($this->config_file,'cke_config'); + + //$this->cke_config += array('custom_config'=>array()); + $this->cke_config += array('plugins'=>array()); + + $this->BuildConfig(); + } + + + /** + * Get the ckeditor build configuration + * + */ + function BuildConfig(){ + global $dataDir; + + + + //get data from build-config.js to determine which plugins are already included + $build_file = $dataDir.'/include/thirdparty/ckeditor/build-config.js'; + $build_config = file_get_contents($build_file); + if( !$build_config ){ + return; + } + + + // quotes + $build_config = str_replace('\'','"',$build_config); + $build_config = str_replace("\r\n", "\n", $build_config); + + // remove comments + $build_config = preg_replace("/\/\*[\d\D]*?\*\//",'',$build_config); + + // remove "var CKBUILDER_CONFIG = " + $pos = strpos($build_config,'{'); + $build_config = substr($build_config,$pos); + $build_config = trim($build_config); + $build_config = trim($build_config,';'); + + // fix variable names + $build_config = preg_replace("/([a-zA-Z0-9_]+?)\s*:/" , "\"$1\":", $build_config); + + $this->build_config = json_decode($build_config,true); + } + + + + + /** + * Output an array in a readable json format + * + */ + static function ReadableJson($mixed){ + static $level = 0; + + if( gettype($mixed) != 'array' ){ + return json_encode($mixed); + } + + + $level++; + + //associative or indexed array + $i = 0; + $indexed = true; + $separator = ' '; + foreach($mixed as $key => $value){ + if( !is_integer($key) && !ctype_digit($key) ){ + $indexed = false; + }elseif( (int)$key !== $i ){ + $indexed = false; + } + if( !$indexed || is_array($value) ){ + $separator = "\n".str_repeat(' ',$level); + } + $i++; + } + + $comma = ''; + if( $indexed ){ + $output = '['; + foreach($mixed as $key => $value){ + $output .= $comma.$separator.self::ReadableJson($value); + $comma = ','; + } + $output .= $separator.']'; + + }else{ + $output = '{'; + foreach($mixed as $key => $value){ + $output .= $comma.$separator.self::ReadableJson($key).' :'.self::ReadableJson($value); + $comma = ','; + } + $output .= $separator.'}'; + } + $level--; + + return $output; + } + +} diff --git a/include/admin/Settings/Classes.php b/include/admin/Settings/Classes.php new file mode 100644 index 0000000..ae85670 --- /dev/null +++ b/include/admin/Settings/Classes.php @@ -0,0 +1,822 @@ +admin_link = \gp\tool::GetUrl('Admin/Classes'); + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'SaveClasses': + $this->SaveClasses(); + break; + } + $this->ClassesForm(); + } + + + /** + * Get the current classes + * + */ + public static function GetClasses(){ + + $classes = \gp\tool\Files::Get('_config/classes'); + if( $classes ){ + array_walk_recursive($classes, function($value){ + return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + }); + return $classes; + } + + //defaults + return self::Defaults(); + } + + + + public static function Defaults(){ + return array( + array( + 'names' => 'gpRow', + 'desc' => \CMS_NAME.' Grid Row - for wrapper sections', + ), + array( + 'names' => 'gpCol-1 gpCol-2 gpCol-3 gpCol-4 gpCol-5 gpCol-6 gpCol-7 gpCol-8 gpCol-9 gpCol-10 gpCol-11 gpCol-12', + 'desc' => \CMS_NAME.' Grid Columns - for content sections', + ), + ); + } + + + + public static function Bootstrap3(){ + return array ( + array ( + 'names' => 'jumbotron', + 'desc' => 'Bootstrap: everything big for calling extra attention to some special content', + ), + array ( + 'names' => 'text-left text-center text-right text-justify', + 'desc' => 'Bootstrap: section text alignment', + ), + array ( + 'names' => 'text-muted text-primary text-success text-info text-warning text-danger', + 'desc' => 'Bootstrap text color classes: will color the entire text in the section (unless otherwise specified)', + ), + array ( + 'names' => 'bg-primary bg-success bg-info bg-warning bg-danger', + 'desc' => 'Bootstrap background color classes: darker backgrounds will also need e.g. text-white', + ), + array ( + 'names' => 'row container container-fluid', + 'desc' => 'Bootstrap Grid: use with Wrapper Sections', + ), + array ( + 'names' => 'col-xs-1 col-xs-2 col-xs-3 col-xs-4 col-xs-5 col-xs-6 col-xs-7 col-xs-8 col-xs-9 col-xs-10 col-xs-11 col-xs-12', + 'desc' => 'Bootstrap Grid: column width (mobile first)', + ), + array ( + 'names' => 'col-sm-1 col-sm-2 col-sm-3 col-sm-4 col-sm-5 col-sm-6 col-sm-7 col-sm-8 col-sm-9 col-sm-10 col-sm-11 col-sm-12', + 'desc' => 'Bootstrap Grid: column width on tablets (screen width ≥ 768px)', + ), + array ( + 'names' => 'col-md-1 col-md-2 col-md-3 col-md-4 col-md-5 col-md-6 col-md-7 col-md-8 col-md-9 col-md-10 col-md-11 col-md-12', + 'desc' => 'Bootstrap Grid: column width on laptops (screen width ≥ 992px)', + ), + array ( + 'names' => 'col-lg-1 col-lg-2 col-lg-3 col-lg-4 col-lg-5 col-lg-6 col-lg-7 col-lg-8 col-lg-9 col-lg-10 col-lg-11 col-lg-12', + 'desc' => 'Bootstrap Grid: column width on desktops (screen width ≥ 1200px)', + ), + array ( + 'names' => 'col-xs-push-1 col-xs-push-2 col-xs-push-3 col-xs-push-4 col-xs-push-5 col-xs-push-6 col-xs-push-7 col-xs-push-8 col-xs-push-9 col-xs-push-10 col-xs-push-11', + 'desc' => 'Bootstrap Grid: push colum to the right (mobile first)', + ), + array ( + 'names' => 'col-sm-push-0 col-sm-push-1 col-sm-push-2 col-sm-push-3 col-sm-push-4 col-sm-push-5 col-sm-push-6 col-sm-push-7 col-sm-push-8 col-sm-push-9 col-sm-push-10 col-sm-push-11', + 'desc' => 'Bootstrap Grid: push colum to the right on tablets (screen width ≥ 768px)', + ), + array ( + 'names' => 'col-md-push-0 col-md-push-1 col-md-push-2 col-md-push-3 col-md-push-4 col-md-push-5 col-md-push-6 col-md-push-7 col-md-push-8 col-md-push-9 col-md-push-10 col-md-push-11', + 'desc' => 'Bootstrap Grid: push colum to the right on laptops (screen width ≥ 992px)', + ), + array ( + 'names' => 'col-lg-push-0 col-lg-push-1 col-lg-push-2 col-lg-push-3 col-lg-push-4 col-lg-push-5 col-lg-push-6 col-lg-push-7 col-lg-push-8 col-lg-push-9 col-lg-push-10 col-lg-push-11', + 'desc' => 'Bootstrap Grid: push colum to the right on desktops (screen width ≥ 1200px)', + ), + array ( + 'names' => 'col-xs-pull-1 col-xs-pull-2 col-xs-pull-3 col-xs-pull-4 col-xs-pull-5 col-xs-pull-6 col-xs-pull-7 col-xs-pull-8 col-xs-pull-9 col-xs-pull-10 col-xs-pull-11', + 'desc' => 'Bootstrap Grid: pull colum to the left (mobile first)', + ), + array ( + 'names' => 'col-sm-pull-0 col-sm-pull-1 col-sm-pull-2 col-sm-pull-3 col-sm-pull-4 col-sm-pull-5 col-sm-pull-6 col-sm-pull-7 col-sm-pull-8 col-sm-pull-9 col-sm-pull-10 col-sm-pull-11', + 'desc' => 'Bootstrap Grid: pull colum to the left on tablets (screen width ≥ 768px)', + ), + array ( + 'names' => 'col-md-pull-0 col-md-pull-1 col-md-pull-2 col-md-pull-3 col-md-pull-4 col-md-pull-5 col-md-pull-6 col-md-pull-7 col-md-pull-8 col-md-pull-9 col-md-pull-10 col-md-pull-11', + 'desc' => 'Bootstrap Grid: pull colum to the left on laptops (screen width ≥ 992px)', + ), + array ( + 'names' => 'col-lg-pull-0 col-lg-pull-1 col-lg-pull-2 col-lg-pull-3 col-lg-pull-4 col-lg-pull-5 col-lg-pull-6 col-lg-pull-7 col-lg-pull-8 col-lg-pull-9 col-lg-pull-10 col-lg-pull-11', + 'desc' => 'Bootstrap Grid: pull colum to the left on desktops (screen width ≥ 1200px)', + ), + array ( + 'names' => 'col-xs-offset-1 col-xs-offset-2 col-xs-offset-3 col-xs-offset-4 col-xs-offset-5 col-xs-offset-6 col-xs-offset-7 col-xs-offset-8 col-xs-offset-9 col-xs-offset-10 col-xs-offset-11', + 'desc' => 'Bootstrap Grid: offset colum to the right (mobile first)', + ), + array ( + 'names' => 'col-sm-offset-0 col-sm-offset-1 col-sm-offset-2 col-sm-offset-3 col-sm-offset-4 col-sm-offset-5 col-sm-offset-6 col-sm-offset-7 col-sm-offset-8 col-sm-offset-9 col-sm-offset-10 col-sm-offset-11', + 'desc' => 'Bootstrap Grid: offset colum to the right on tablets (screen width ≥ 768px)', + ), + array ( + 'names' => 'col-md-offset-0 col-md-offset-1 col-md-offset-2 col-md-offset-3 col-md-offset-4 col-md-offset-5 col-md-offset-6 col-md-offset-7 col-md-offset-8 col-md-offset-9 col-md-offset-10 col-md-offset-11', + 'desc' => 'Bootstrap Grid: offset colum to the right on laptops (screen width ≥ 992px)', + ), + array ( + 'names' => 'col-lg-offset-0 col-lg-offset-1 col-lg-offset-2 col-lg-offset-3 col-lg-offset-4 col-lg-offset-5 col-lg-offset-6 col-lg-offset-7 col-lg-offset-8 col-lg-offset-9 col-lg-offset-10 col-lg-offset-11', + 'desc' => 'Bootstrap Grid: offset colum to the right on desktops (screen width ≥ 1200px)', + ), + array ( + 'names' => 'visible-xs-block visible-sm-block visible-md-block visible-lg-block', + 'desc' => 'Bootstrap Visibility: using this classes will make the section visible only on the specified breakpoint / screen width (xs, sm, md or lg)', + ), + array ( + 'names' => 'hidden-xs hidden-sm hidden-md hidden-lg', + 'desc' => 'Bootstrap Visibility: using this classes will hide the section only on the specified breakpoint / screen width (xs, sm, md or lg)', + ), + ); + } + + + public static function Bootstrap4(){ + + $cols_count = 12; + $breakpoints = [ 'xs', 'sm', 'md', 'lg', 'xl' ]; + $colors = [ 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark' ]; + $spacers = range(0, 5); + $margins = array_merge(range(0, 5), ['auto', 'n1', 'n2', 'n3', 'n4', 'n5']); + $cols = array_merge([''], range(1, $cols_count), ['auto']); + $offsets = range(0, $cols_count-1); + + $bs4 = []; // array that will be returned + + function addSet(&$bs4, $desc, $name, $bps, $vals){ // $bs4 is passed by reference! + $breakpoint_descs = [ + 'xs' => '(mobile first)', + 'sm' => 'on large smartphones (screen width ≥ 576px)', + 'md' => 'on tablets (screen width ≥ 786px)', + 'lg' => 'on laptops (screen width ≥ 992px)', + 'xl' => 'on desktops (screen width ≥ 1200px)', + ]; + foreach( $bps as $i => $bp ){ + $names = []; + $d = $desc; + $bpn = $bp == 'xs' || $bp == '' ? '' : '-' . $bp; + foreach( $vals as $val ){ + if( $bp == 'xs' && $name == 'offset' && $val == 0 ){ + continue; + } + $names[] = $name . $bpn . ($val !== '' ? '-' : '') . $val; + } + if( !empty($bp) && !empty($breakpoint_descs[$bp]) ){ + $d .= ' ' . $breakpoint_descs[$bp]; + } + $bs4[] = [ + 'names' => implode(' ', $names), + 'desc' => $d, + ]; + } + } + + // text + addSet( + $bs4, + 'BS4: text alignment', + 'text', + $breakpoints, + ['left', 'center', 'right', 'justify'] + ); + $bs4[] = [ + 'names' => 'text-primary text-secondary text-success text-danger text-warning ' . + 'text-info text-light text-dark text-white text-body text-muted ' . + 'text-black-50 text-white-50 text-reset', + 'desc' => 'BS4 text utils: colors the entire text in the section (unless otherwise specified)', + ]; + $bs4[] = [ + 'names' => 'font-weight-normal font-weight-bold font-weight-bolder font-weight-light font-weight-lighter', + 'desc' => 'BS4 text utils: apply different font weights', + ]; + $bs4[] = [ + 'names' => 'font-italic', + 'desc' => 'BS4 text utils: use italic font style', + ]; + $bs4[] = [ + 'names' => 'text-monospace', + 'desc' => 'BS4 text utils: use monospace font (stack) defined in variables.scss', + ]; + $bs4[] = [ + 'names' => 'lead', + 'desc' => 'BS4 text utils: makes paragraphs

        inside the section stand out. Does not influence headings and other elements with defined font sizes', + ]; + $bs4[] = [ + 'names' => 'small', + 'desc' => 'BS4 text utils: makes text inside the section smaller. Does not influence headings and other elements with defined font sizes', + ]; + $bs4[] = [ + 'names' => 'text-lowercase text-uppercase text-capitalize', + 'desc' => 'BS4 text utils: use text-transform to change case', + ]; + $bs4[] = [ + 'names' => 'text-nowrap text-truncate', + 'desc' => 'BS4 text utils: prevent text from wrapping or truncate it', + ]; + $bs4[] = [ + 'names' => 'text-break', + 'desc' => 'BS4 text utils: force long words to break at the section boundaries', + ]; + + // background colors + $bs4[] = [ + 'names' => 'bg-primary bg-secondary bg-success bg-danger bg-warning bg-info bg-light bg-dark bg-white bg-transparent', + 'desc' => 'BS4 background colors: darker backgrounds will also need e.g. text-white', + ]; + + /* + // background gradients (disabled by default) + $bs4[] = [ + 'names' => 'bg-gradient-primary bg-gradient-secondary bg-gradient-success bg-gradient-danger bg-gradient-warning bg-gradient-info bg-gradient-light bg-gradient-dark', + 'desc' => 'BS4 background gradients: only works with $enable-gradients: true; in variables.scss' + ]; + */ + + // containers + rows + $bs4[] = [ + 'names' => 'row container container-fluid', + 'desc' => 'BS4 layout/grid: to be used with wrapper sections', + ]; + + // row-cols + addSet( + $bs4, + 'BS4 grid: use together with ‘row’ to control how many col child sections appear next to each other', + 'row-cols', + $breakpoints, + range(1, $cols_count) + ); + + // no-gutters + $bs4[] = [ + 'names' => 'no-gutters', + 'desc' => 'BS4 grid: use together with ‘row’ to remove its negative margins ' . + 'and the horizontal padding from all immediate child cols', + ]; + + // columns + addSet( + $bs4, + 'BS4 grid: column width (in twelfths)', + 'col', + $breakpoints, + $cols + ); + + // offsets + addSet( + $bs4, + 'BS4 grid: offset a colum to the right (in twelfths)', + 'offset', + $breakpoints, + $offsets + ); + + // display + addSet( + $bs4, + 'BS4 display utils: e.g. use d-none to hide an element', + 'd', + $breakpoints, + ['none', 'flex', 'inline-flex', 'block', 'inline', 'inline-block', 'table', 'table-cell', 'table-row'] + ); + + /* + // vertical-align + // Disabled for being potentially misleading with sections, + // which are very unlinkely to be inline level or table cells + $bs4[] = [ + 'names' => 'align-baseline align-top align-middle align-bottom align-text-bottom align-text-top', + 'desc' => 'BS4 alignment utils: change vertical alignment of a section. ' . + 'Only works with d-inline, d-inline-block, d-inline-table or table-cell', + ]; + */ + + // flex + addSet( + $bs4, + 'BS4 flex utils: direction of flex items in a flex container', + 'flex', + $breakpoints, + ['row', 'column', 'row-reverse', 'column-reverse'] + ); + addSet( + $bs4, + 'BS4 flex utils: change how flex items wrap in a flex container', + 'flex', + $breakpoints, + ['wrap', 'nowrap', 'wrap-reverse'] + ); + addSet( + $bs4, + 'BS4 flex utils: change the alignment of flex items on the main axis (flex-row=horizontal, flex-column=vertical)', + 'justify-content', + $breakpoints, + ['start', 'end', 'center', 'between', 'around'] + ); + addSet( + $bs4, + 'BS4 flex utils: changes how flex items align together on the cross axis (flex-row=vertical, flex-column=horizontal)', + 'align-content', + $breakpoints, + ['start', 'end', 'center', 'around', 'stretch'] + ); + addSet( + $bs4, + 'BS4 flex utils: change the alignment of flex items on the cross axis (flex-row=vertical, flex-column=horizontal)', + 'align-items', + $breakpoints, + ['start', 'end', 'center', 'baseline', 'stretch'] + ); + addSet( + $bs4, + 'BS4 flex utils: use on flexbox items to individually change their alignment on the cross axis', + 'align-self', + $breakpoints, + ['start', 'end', 'center', 'baseline', 'stretch'] + ); + addSet( + $bs4, + 'BS4 flex utils: use on series of sibling elements to force them into widths equal to their content (similar to table cells)', + 'flex', + $breakpoints, + ['fill'] + ); + addSet( + $bs4, + 'BS4 flex utils: toggle a flex item’s ability to grow to fill available space', + 'flex', + $breakpoints, + ['grow-0', 'grow-1'] + ); + addSet( + $bs4, + 'BS4 flex utils: toggle a flex item’s ability to shrink if necessary', + 'flex', + $breakpoints, + ['shrink-0', 'shrink-1'] + ); + + // cards + $bs4[] = [ + 'names' => 'card-columns card-deck card-group', + 'desc' => 'BS4 card layout wrappers: use for wrapper sections that contain ‘card’ sections. ' . + 'card-columns: a pinterest-like masonry, ' . + 'card-deck: grid of cards of equal height and width, ' . + 'card-group: similar to grid but without gutters', + ]; + $bs4[] = [ + 'names' => 'card', + 'desc' => 'BS4 card element: use this class on wrapper sections', + ]; + $bs4[] = [ + 'names' => 'card-header card-body card-footer', + 'desc' => 'BS4 card content: use for child sections inside wrapper sections with the ‘card’ class', + ]; + $bs4[] = [ + 'names' => 'card-img card-img-top card-img-bottom', + 'desc' => 'BS4 card content: use for child image sections inside wrapper sections with the ‘card’ class', + ]; + $bs4[] = [ + 'names' => 'card-img-overlay', + 'desc' => 'BS4 card content: use for child sections inside wrapper sections with the ‘card’ class. The section must follow a ‘card-image’ section so its content can overlay the image', + ]; + $bs4[] = [ + 'names' => 'card-title card-subtitle card-text', + 'desc' => 'BS4 card content: use for child sections inside wrapper sections with the ‘card-header -body or -footer’ classes', + ]; + + + // alerts + $bs4[] = [ + 'names' => 'alert', + 'desc' => 'BS4 alert: a message-box-style section. Use together with alert-color classes', + ]; + addSet( + $bs4, + 'BS4 alerts: color classes to be used together with the ‘alert’ class', + 'alert', + [''], + $colors + ); + + + // overflow + $bs4[] = [ + 'names' => 'overflow-auto overflow-hidden', + 'desc' => 'BS4 utils: determines how content overflows the section', + ]; + + // position + $bs4[] = [ + 'names' => 'position-relative position-absolute position-fixed position-sticky position-static fixed-top fixed-bottom sticky-top', + 'desc' => 'BS4 utils: determines the positioning of the section', + ]; + + //paddings + addSet( + $bs4, + 'BS4 sizing utils: set padding on all 4 sides', + 'p', + $breakpoints, + $spacers + ); + addSet( + $bs4, + 'BS4 sizing utils: set both padding-left and padding-right', + 'px', + $breakpoints, + $spacers + ); + addSet( + $bs4, + 'BS4 sizing utils: set padding-left', + 'pl', + $breakpoints, + $spacers + ); + addSet( + $bs4, + 'BS4 sizing utils: set padding-right', + 'pr', + $breakpoints, + $spacers + ); + addSet( + $bs4, + 'BS4 sizing utils: set both padding-top and padding-bottom', + 'py', + $breakpoints, + $spacers + ); + addSet( + $bs4, + 'BS4 sizing utils: set padding-top', + 'pt', + $breakpoints, + $spacers + ); + addSet( + $bs4, + 'BS4 sizing utils: set padding-bottom', + 'pb', + $breakpoints, + $spacers + ); + + // margins + addSet( + $bs4, + 'BS4 sizing utils: set margin on all 4 sides', + 'm', + $breakpoints, + $margins + ); + addSet( + $bs4, + 'BS4 sizing utils: set both margin-left and margin-right', + 'mx', + $breakpoints, + $margins + ); + addSet( + $bs4, + 'BS4 sizing utils: set margin-left', + 'ml', + $breakpoints, + $margins + ); + addSet( + $bs4, + 'BS4 sizing utils: set margin-right', + 'mr', + $breakpoints, + $margins + ); + addSet( + $bs4, + 'BS4 sizing utils: set both margin-top and margin-bottom', + 'my', + $breakpoints, + $margins + ); + addSet( + $bs4, + 'BS4 sizing utils: set margin-top', + 'mt', + $breakpoints, + $margins + ); + addSet( + $bs4, + 'BS4 sizing utils: set margin-bottom', + 'mb', + $breakpoints, + $margins + ); + + // width + addSet( + $bs4, + 'BS4 sizing utils: quickly define or override an element’s width', + 'w', + $breakpoints, + ['25', '50', '75', '100', 'auto'] + ); + + // max-width + addSet( + $bs4, + 'BS4 sizing utils: quickly define or override an element’s max-width', + 'w', + $breakpoints, + ['100'] + ); + + // height + addSet( + $bs4, + 'BS4 sizing utils: quickly define or override an element’s height', + 'h', + $breakpoints, + ['25', '50', '75', '100', 'auto'] + ); + + // max-height + addSet( + $bs4, + 'BS4 sizing utils: quickly define or override an element’s max-height', + 'h', + $breakpoints, + ['100'] + ); + + // order + addSet( + $bs4, + 'BS4 order utils: change the visual order of the section inside its wrapper', + 'order', + $breakpoints, + array_merge(['order-first', 'order-last'], range(0, 12)) + ); + + // border + $bs4[] = [ + 'names' => 'border border-top border-right border-bottom border-left', + 'desc' => 'BS4 border utils: add borders to an element', + ]; + $bs4[] = [ + 'names' => 'border-0 border-top-0 border-right-0 border-bottom-0 border-left-0', + 'desc' => 'BS4 border utils: subtract an element’s borders', + ]; + addSet( + $bs4, + 'BS4 border utils: change the border color', + 'border', + [''], // not responsive but we need to pass an array + $colors + ); + + // border-radius + $bs4[] = [ + 'names' => 'rounded rounded-top rounded-right rounded-bottom rounded-left rounded-circle rounded-pill rounded-0', + 'desc' => 'BS4 border utils: easily round an element’s corners', + ]; + $bs4[] = [ + 'names' => 'rounded-sm rounded-lg', + 'desc' => 'BS4 border utils: use for larger or smaller border-radius', + ]; + + /* + // shadows (disabled by default) + $bs4[] = [ + 'names' => 'shadow shadow-none shadow-sm shadow-lg', + 'desc' => 'BS4: change shadow display and size added via box-shadow utility classes. Requires $enable-shadows: true; in variables.scss', + ]; + */ + + // jumbotron + $bs4[] = [ + 'names' => 'jumbotron', + 'desc' => 'BS4: everything big for calling extra attention to some special content', + ]; + $bs4[] = [ + 'names' => 'jumbotron-fluid', + 'desc' => 'BS4: combine with jumbotron for full-width sections without rounded corners', + ]; + + // float + addSet( + $bs4, + 'BS4 float utils: toggle floats on the section', + 'float', + $breakpoints, + ['left', 'right', 'none'] + ); + + // clearfix + $bs4[] = [ + 'names' => 'clearfix', + 'desc' => 'BS clearfix: use for wrapper sections that contain floated child sections', + ]; + + // visibility + $bs4[] = [ + 'names' => 'visible invisible', + 'desc' => 'BS4 visibility: control the visibility without modifying the display. Invisible elements will still take up space in the page', + ]; + + // screen readers only + $bs4[] = [ + 'names' => 'sr-only', + 'desc' => 'BS4 screen reader utils: hide elements on all devices except screen readers', + ]; + $bs4[] = [ + 'names' => 'sr-only-focusable', + 'desc' => 'BS4 screen reader utils: combine with sr-only to show the element again when it’s focused (e.g. via keyboard)', + ]; + + // print + $bs4[] = [ + 'names' => 'd-print-none d-print-inline d-print-inline-block d-print-block d-print-table d-print-table-row d-print-table-cell d-print-flex d-print-inline-flex', + 'desc' => 'BS4 print utils: change the display value of elements when printing', + ]; + + return $bs4; + } + + + + /** + * Display form for selecting classes + * + */ + private function ClassesForm(){ + global $dataDir, $langmessage; + + echo '

        ' . $langmessage['Manage Classes'] . '

        '; + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'LoadDefault': + $loaded_classes = self::Defaults(); + break; + + case 'LoadBootstrap3': + $loaded_classes = self::Bootstrap3(); + break; + + case 'LoadBootstrap4': + $loaded_classes = self::Bootstrap4(); + break; + + default: + $loaded_classes = self::GetClasses(); + break; + } + + $classes = self::GetClasses(); + + $processing = !empty($_REQUEST['process']) ? $_REQUEST['process'] : 'load'; + + switch($processing){ + case 'prepend': + $classes = array_unique(array_merge($loaded_classes, $classes), SORT_REGULAR); + break; + + case 'append': + $classes = array_unique(array_merge($classes, $loaded_classes), SORT_REGULAR); + break; + + case 'remove': + $classes = array_udiff($classes, $loaded_classes, function($a, $b){ + return strcmp($a['names'], $b['names']); + }); + break; + + case 'load': + $classes = $loaded_classes; + break; + } + + // the following is not beautiful ;) + if( $cmd && $cmd != 'SaveClasses'){ + msg('OK. ' + . $langmessage['save'] . ' (?)'); + } + + $classes[] = array('names'=>'','desc'=>''); + + + $this->page->jQueryCode .= '$(".sortable_table").sortable({items : "tr",handle: "td"});'; + + // FORM + echo '
        '; + echo ''; + echo ''; + echo ''; + + foreach( $classes as $key => $classArray ){ + echo ''; + } + + echo ''; + echo ''; + echo '
        ' . $langmessage['Classes'] . '' . $langmessage['description'] . '
        '; + echo '   '; + echo ''; + echo ''; + echo ' '; + echo ''; + echo '
        '; + echo '' . $langmessage['add'] . ''; + echo '
        '; + + // SAVE / CANCEL BUTTONS + echo '
        '; + echo ''; + echo ''; + + echo '
        '; + + echo '
        '; + echo $langmessage['Manage Classes Description']; + echo '
        '; + + echo '

        ' . $langmessage['Load'] . ', ' . $langmessage['Merge'] . ', ' . $langmessage['remove'] . '

        '; + + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
        '; + echo '
        '; + + } + + + /** + * Save the posted data + * + */ + public function SaveClasses(){ + global $langmessage; + + $classes = array(); + foreach($_POST['class_names'] as $i => $class_names){ + + $class_names = trim($class_names); + if( empty($class_names) ){ + continue; + } + + $classes[] = array( + 'names' => $class_names, + 'desc' => $_POST['class_desc'][$i], + ); + } + + + if( \gp\tool\Files::SaveData('_config/classes','classes',$classes) ){ + msg($langmessage['SAVED']); + }else{ + msg($langmessage['OOPS'].' (Not Saved)'); + } + } + + +} diff --git a/include/admin/Settings/Missing.php b/include/admin/Settings/Missing.php new file mode 100644 index 0000000..8a2dbe8 --- /dev/null +++ b/include/admin/Settings/Missing.php @@ -0,0 +1,659 @@ +page = $args['page']; + $this->codes = [ + '301' => $langmessage['301 Moved Permanently'], + '302' => $langmessage['302 Moved Temporarily'], + ]; + + \gp\tool\Editing::PrepAutoComplete(); + + $this->cmds['Save404'] = 'Edit404'; + $this->cmds['Edit404'] = ''; + $this->cmds['EditRedir'] = ''; + $this->cmds['UpdateRedir'] = 'DefaultDisplay'; + $this->cmds['SaveRedir'] = 'DefaultDisplay'; + $this->cmds['RmRedir'] = 'DefaultDisplay'; + } + + + /** + * Add instructions for a 301 or 302 redirect + * + */ + public static function AddRedirect($source,$target){ + global $dataDir; + + $datafile = $dataDir . '/data/_site/error_data.php'; + $error_data = \gp\tool\Files::Get('_site/error_data'); + if( !$error_data ){ + $error_data = []; + } + + $changed = false; + + //remove redirects from the $target + if( isset($error_data['redirects'][$target]) ){ + unset($error_data['redirects'][$target]); + $changed = true; + } + + //redirect already exists for $source + if( !isset($error_data['redirects'][$source]) ){ + $error_data['redirects'][$source]['target'] = $target; + $error_data['redirects'][$source]['code'] = '301'; + $changed = true; + } + + if( $changed ){ + \gp\tool\Files::SaveData($datafile, 'error_data', $error_data); + } + } + + + protected function SaveMissingData(){ + global $langmessage; + + if( !\gp\tool\Files::SaveData($this->datafile, 'error_data', $this->error_data) ){ + msg($langmessage['OOPS']); + return false; + } + + msg($langmessage['SAVED']); + return true; + } + + protected function GetCodeLanguage($code){ + global $langmessage; + switch($code){ + case '301': + return $langmessage['301 Moved Permanently']; + case '302': + return $langmessage['302 Moved Temporarily']; + } + return ''; + } + + + /** + * Show 404 info and Redirection list + * + */ + public function DefaultDisplay(){ + global $langmessage; + + echo '

        ' . $langmessage['Link Errors'] . '

        '; + echo '

        ' . $langmessage['404_Usage'] . '

        '; + + //404 Page + echo '

        ' . $langmessage['404_Page'] . '

        '; + + echo '
        '; + echo '

        ' . $langmessage['About_404_Page'] . '

        '; + echo '

        '; + echo \gp\tool::Link('Special_Missing', + $langmessage['preview'], + '', + ['class' => 'gpsubmit'] + ); + echo '   '; + echo \gp\tool::Link('Admin/Missing', + $langmessage['edit'], + 'cmd=Edit404', + ['class' => 'gpsubmit'] + ); + echo '

        '; + echo '
        '; + + //redirection + echo '
        '; + echo '

        ' . $langmessage['Redirection'] . '

        '; + echo '
        '; + $this->ShowRedirection(); + echo '
        '; + } + + + protected function Save404(){ + + $text =& $_POST['gpcontent']; + \gp\tool\Files::cleanText($text); + $this->error_data['404_TEXT'] = $text; + + if( $this->SaveMissingData() ){ + return true; + } + + $this->Edit404($text); + return false; + } + + + /** + * Display form for editing the 404 page content + * + */ + protected function Edit404($text=null){ + global $langmessage; + + if( is_null($text) ){ + if( isset($this->error_data['404_TEXT']) ){ + $text = $this->error_data['404_TEXT']; + }else{ + $text = self::DefaultContent(); + } + } + + echo '

        '; + echo \gp\tool::Link('Admin/Missing', $langmessage['Link Errors']); + echo ' » '; + echo $langmessage['404_Page']; + echo '

        '; + + echo '
        '; + echo ''; + + \gp\tool\Editing::UseCK($text); + + echo ''; + echo ' '; + echo '
        '; + + echo '
        '; + + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
        '; + echo $langmessage['Useful Variables']; + echo '
        '; + echo '{{Similar_Titles}}'; + echo ''; + echo $langmessage['Similar_Titles']; + echo '
        '; + } + + + /** + * Display current redirection settings + * + */ + protected function ShowRedirection(){ + global $langmessage, $gp_index, $config; + + $this->page->head_js[] = '/include/thirdparty/tablesorter/tablesorter.js'; + $this->page->jQueryCode .= '$("table.tablesorter").tablesorter({' . + 'cssHeader : "gp_header",' . + 'cssAsc : "gp_header_asc",' . + 'cssDesc : "gp_header_desc"' . + '});'; + $has_invalid_target = false; + + + echo '

        '; + echo $langmessage['About_Redirection']; + echo '

        '; + + echo '
        '; + + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + echo ''; + + foreach($this->error_data['redirects'] as $source => $data){ + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo ''; + + echo ''; + } + echo ''; + + $this->AddMissingRow(); + echo '
        ' . $langmessage['Source URL'] . '' . $langmessage['Target URL'] . '' . $langmessage['Similarity'] . '' . $langmessage['Method'] . '' . $langmessage['options'] . '
        '; + $raw_source = $source; + if( !empty($data['raw_source']) ){ + $raw_source = $data['raw_source']; + } + echo \gp\tool::GetUrl(''); + echo htmlspecialchars($raw_source); + echo ''; + $target_show = $data['target']; + if( strlen($target_show) > 40 ){ + // truncate middle + $target_show = substr($target_show, 0, 15) . ' … ' . substr($target_show, -15); + } + + $full_target = $this->GetTarget($data['target'], false); + $is_gplink = $this->isGPLink($data['target']); + $valid_target = $this->ValidTarget($data['target']); + + if( !$valid_target ){ + $has_invalid_target = true; + echo ' '; + echo '   '; + } + + echo ''; + echo str_replace(' ', ' ', htmlspecialchars($target_show)); + echo ''; + + echo ''; + if( $is_gplink ){ + $lower_source = strtolower($raw_source); + $lower_target = strtolower($target_show); + similar_text($lower_source, $lower_target, $percent); + echo number_format($percent, 1) . '%'; + } + echo ' '; + echo $this->GetCodeLanguage($data['code']); + echo ''; + echo \gp\tool::Link( + 'Admin/Missing', + $langmessage['edit'], + 'cmd=EditRedir&source=' . urlencode($source), + ['data-cmd' => 'gpabox'] + ); + + echo '   '; + echo \gp\tool::Link($source, $langmessage['Test']); + + echo '   '; + $title = sprintf($langmessage['generic_delete_confirm'], $source); + echo \gp\tool::Link( + 'Admin/Missing', + $langmessage['delete'], + 'cmd=RmRedir&link=' . urlencode($source), + [ + 'data-cmd' => 'postlink', + 'title' => $title, + 'class' => 'gpconfirm', + ] + ); + echo '
        '; + echo '
        '; + + echo '
        '; + + if( $has_invalid_target ){ + echo '

        '; + echo '     '; + echo $langmessage['Target URL Invalid']; + echo ''; + echo '

        '; + } + } + + + /** + * Return true if the target is a valid url + * + * @return bool + */ + public function ValidTarget($target){ + global $gp_index; + + if( empty($target) ){ + return true; + } + + if( !$this->isGPLink($target) ){ + return true; + } + + if( isset($gp_index[$target]) ){ + return true; + } + + $type = \gp\tool::SpecialOrAdmin($target); + if( $type == 'admin' ){ + return true; + } + + return false; + } + + + /** + * Add Redirection form for + * + */ + protected function AddMissingRow(){ + global $langmessage; + + $_REQUEST += [ + 'source' => '', + 'target' => '', + 'code' => '', + 'orig_source' => '', + ]; + + echo ''; + echo ''; + + //source + echo ''; + echo \gp\tool::GetUrl(''); + echo ''; + echo ''; + + //target + echo ''; + echo ''; + echo ''; + + echo ''; + + //code + echo ''; + $this->CodeSelect($_REQUEST['code']); + echo ''; + + echo ''; + echo ''; + echo ''; + + echo ''; + echo ''; + } + + + /** + * Edit an existing redirection + * + */ + protected function EditRedir(){ + global $langmessage; + + $source = \gp\admin\Tools::PostedSlug($_REQUEST['source']); + if( !isset($this->error_data['redirects'][$source]) ){ + msg($langmessage['OOPS'] . ' (Invalid Redirect)'); + return false; + } + + $args = $this->error_data['redirects'][$source]; + $args['cmd'] = 'updateredir'; + $args['orig_source'] = $source; + $args['source'] = $source; + + $this->RedirForm($args); + } + + + /** + * Using inline_box for this one for autocomplete init + * + */ + protected function RedirForm($values=[]){ + global $langmessage, $gp_index; + + $values += [ + 'cmd' => 'saveredir', + 'source' => '', + 'target' => '', + 'code' => '', + 'orig_source' => '', + ]; + + + echo '
        '; + echo '

        ' . $langmessage['New Redirection'] . '

        '; + + echo '
        '; + echo ''; + echo ''; + + echo ''; + + echo ''; + echo ''; + echo ''; + + //source url + echo ''; + echo ''; + echo ''; + echo ''; + + //method + echo ''; + echo ''; + echo ''; + echo ''; + + //target url + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
        ' . $langmessage['options'] . '
        '; + echo $langmessage['Source URL']; + echo ''; + echo \gp\tool::GetUrl(''); + echo ''; + echo '
        '; + echo $langmessage['Method']; + echo ''; + $this->CodeSelect($values['code']); + echo '
        '; + echo $langmessage['Target URL']; + echo ''; + echo ''; + echo '
        '; + + echo '

        '; + echo ''; + echo ' '; + echo '

        '; + + echo '
        '; + echo '
        '; + } + + + /** + * Display select for redirect code + * + */ + protected function CodeSelect($value){ + echo ''; + } + + + protected function CheckRedir(){ + global $langmessage; + + if( empty($_POST['source']) ){ + msg($langmessage['OOPS'].' (Empty Source)'); + return false; + } + + if( $_POST['source'] == $_POST['target'] ){ + msg($langmessage['OOPS'].' (Infinite Loop)'); + return false; + } + + if( \gp\admin\Tools::PostedSlug($_POST['source']) == + \gp\admin\Tools::PostedSlug($_POST['target']) + ){ + msg($langmessage['OOPS'] . ' (Infinite Loop)'); + return false; + } + + if( $_POST['code'] != '302' ){ + $_POST['code'] = 301; + } + + return true; + } + + + /** + * Update the settings for an existing redirection + * + */ + protected function UpdateRedir(){ + global $langmessage; + + if( !$this->CheckRedir() ){ + return false; + } + + $orig_source = $_POST['orig_source']; + $source = \gp\admin\Tools::PostedSlug($orig_source); + + if( !isset($this->error_data['redirects'][$orig_source]) ){ + msg($langmessage['OOPS'] . ' (Entry not found)'); + return false; + } + + $data = []; + $data['target'] = $_POST['target']; + $data['code'] = $_POST['code']; + $data['raw_source'] = $_POST['source']; + + if( + !\gp\tool\Files::ArrayReplace( + $orig_source, + $source, + $data, + $this->error_data['redirects'] + ) + ){ + msg($langmessage['OOPS']); + return false; + } + + return $this->SaveMissingData(); + } + + + /** + * Save a new redirection + * + */ + protected function SaveRedir(){ + global $langmessage, $gp_index; + + if( !$this->CheckRedir() ){ + return false; + } + + $source = \gp\admin\Tools::PostedSlug($_POST['source']); + + if( isset($this->error_data['redirects'][$source]) ){ + msg($langmessage['OOPS'] . ' (Redirect Already Set)'); + return false; + } + + $redirect = [ + 'target' => $_POST['target'], + 'code' => (int)$_POST['code'], + 'source' => $_POST['source'], + ]; + + $this->error_data['redirects'][$source] = $redirect; + + return $this->SaveMissingData(); + } + + + /** + * Remove a redirection + * + */ + protected function RmRedir(){ + global $langmessage; + + $link =& $_POST['link']; + if( !isset($this->error_data['redirects'][$link]) ){ + msg($langmessage['OOPS']); + return false; + } + + unset($this->error_data['redirects'][$link]); + return $this->SaveMissingData(); + } + +} diff --git a/include/admin/Settings/Permalinks.php b/include/admin/Settings/Permalinks.php new file mode 100644 index 0000000..17cef2b --- /dev/null +++ b/include/admin/Settings/Permalinks.php @@ -0,0 +1,742 @@ +server_name = \gp\tool::ServerName(true); + + //get current rules + $this->rule_file_name = self::IIS() ? 'web.config' : '.htaccess'; + $this->rule_file = $dataDir.'/'.$this->rule_file_name; + if( file_exists($this->rule_file) ){ + $this->orig_rules = file_get_contents($this->rule_file); + } + + + $this->FileSystem = \gp\tool\FileSystem::init($this->rule_file); + $this->WWWAvail(); + + echo '

        '.$langmessage['permalink_settings'].'

        '; + + + $cmd = \gp\tool::GetCommand(); + switch($cmd){ + case 'continue': + if( !$this->SaveHtaccess() ){ + break; + } + + default: + $this->ShowForm(); + break; + } + } + + + /** + * Determine if we're able to change the www redirect of the server + * + */ + public function WWWAvail(){ + + if( !$this->server_name ){ + return; + } + + if( self::IIS() ){ + return; + } + + + // already has www settings? + if( !is_null($this->orig_rules) ){ + if( strpos($this->orig_rules,'# with www') !== false ){ + $this->www_setting = true; + $this->www_avail = true; + return; + } + if( strpos($this->orig_rules,'# without www') !== false ){ + $this->www_setting = false; + $this->www_avail = true; + return; + } + } + + + // check non-www site + $url = $this->WWWUrl(false,'special_site_map'); + if( !self::ConfirmGet($url) ){ + return; + } + + + // check www site + $url = $this->WWWUrl(true,'special_site_map'); + if( !self::ConfirmGet($url) ){ + return; + } + + + $this->www_avail = true; + } + + + /** + * Return url with or without www + * + */ + public function WWWUrl($with_www = true, $slug = ''){ + + $schema = ( isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on' ) ? 'https://' : 'http://'; + $host = $this->server_name; + + if( $with_www ){ + $host = 'www.'.$this->server_name; + } + + return $schema.$host.\gp\tool::GetUrl($slug,'',false); + } + + + /** + * Confirm getting the url retreives the current installation + * + */ + public static function ConfirmGet($url, $check_redirect = true ){ + global $config; + + $result = \gp\tool\RemoteGet::Get_Successful($url); + + if( !$result ){ + return false; + } + + if( isset($config['gpuniq']) ){ + $mdu_check = substr(md5($config['gpuniq']),0,20); + + if( strpos($result,$mdu_check) === false ){ + return false; + } + } + + //if redirected, make sure it's on the same host + if( $check_redirect && \gp\tool\RemoteGet::$redirected ){ + $redirect_a = parse_url(\gp\tool\RemoteGet::$redirected); + $req_a = parse_url($url); + if( $redirect_a['host'] !== $req_a['host'] ){ + return false; + } + } + + + return true; + } + + + /** + * Display Permalink Options + * + */ + public function ShowForm(){ + global $langmessage; + + + + $confirmed_mod_rewrite = false; + if( function_exists('apache_get_modules') ){ + $mods = apache_get_modules(); + if( in_array('mod_rewrite',$mods) !== false ){ + $confirmed_mod_rewrite = true; + } + } + + if( !$confirmed_mod_rewrite ){ + echo '

        '; + echo $langmessage['limited_mod_rewrite']; + echo '

        '; + } + + echo '
        '; + echo '
        '; + echo ''; + + echo ''; + + //default + $checked = ''; + if( !$_SERVER['gp_rewrite'] ){ + $checked = 'checked="checked"'; + } + + echo ''; + + //hide index.php + $checked = ''; + if( $_SERVER['gp_rewrite'] ){ + $checked = 'checked="checked"'; + } + + echo ''; + + //www + if( $this->www_avail ){ + echo ''; + + $checked = 'checked'; + echo ''; + + + //without www + $checked = ($this->www_setting === false) ? 'checked' : ''; + echo ''; + + + //with www + $checked = ($this->www_setting === true) ? 'checked' : ''; + echo ''; + } + + + echo '
        index.php
        '; + echo ''; + + echo ''; + echo '
        ';
        +		echo $this->ExampleUrl(true);
        +		echo '
        '; + echo '
        '; + echo ''; + + echo ''; + echo '
        ';
        +		echo $this->ExampleUrl(false);
        +		echo '
        '; + echo '
        www
        '; + echo ''; + echo ''; + echo '
        ';
        +			echo $this->WWWUrl(false);
        +			echo '
        '; + echo ' & '; + echo '
        ';
        +			echo $this->WWWUrl(true);
        +			echo '
        '; + echo '
        '; + echo ''; + + echo ''; + echo '
        ';
        +			echo $this->WWWUrl(false);
        +			echo '
        '; + echo '
        '; + echo ''; + + echo ''; + echo '
        ';
        +			echo $this->WWWUrl(true);
        +			echo '
        '; + echo '
        '; + + echo '
        '; + echo '

        '; + echo ''; + echo ''; + echo '

        '; + + echo '
        '; + + } + + + /** + * Return an example url based on potential gp_rewrite setting + * + */ + public function ExampleUrl($index_php){ + global $dirPrefix; + + $schema = ( isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on' ) ? 'https://' : 'http://'; + $temp_prefix = $index_php ? $dirPrefix.'/index.php' : $dirPrefix; + + if( $this->server_name ){ + return $schema.$this->server_name.$temp_prefix.'/sample-page'; + } + + return $temp_prefix; + } + + + /** + * Determine how to save the htaccess file to the server (ftp,direct,manual) and give user the appropriate options + * + * @return boolean true if the .htaccess file is saved + */ + public function SaveHtaccess(){ + global $langmessage, $dirPrefix; + + + //hide index ? + if( isset($_POST['rewrite_setting']) && $_POST['rewrite_setting'] == 'hide_index' ){ + $this->hide_index = true; + $this->undo_if_failed = true; + } + + // www preference + $www = null; + if( isset($_POST['www_setting']) ){ + if( $_POST['www_setting'] === 'with' ){ + $www = true; + $this->undo_if_failed = true; + + }elseif( $_POST['www_setting'] === 'without' ){ + $www = false; + $this->undo_if_failed = true; + } + } + + + $this->new_rules = self::Rewrite_Rules( $this->hide_index, $dirPrefix, $this->orig_rules, $www ); + + + // only proceed with hide if we can test the results + if( !$this->CanTestRules() ){ + $this->ManualMethod(); + return false; + } + + + if( !$this->SaveRules() ){ + $this->FileSystem->CompleteForm($_POST,'Admin/Permalinks'); + $this->ManualMethod(); + return false; + } + + msg($langmessage['SAVED']); + + //redirect to new permalink structure + $_SERVER['gp_rewrite'] = $this->hide_index; + \gp\tool::SetLinkPrefix(); + $redir = \gp\tool::GetUrl('Admin/Permalinks'); + \gp\tool::Redirect($redir,302); + + return false; + } + + + /** + * Determine if we will be able tot test the results + * + */ + public function CanTestRules(){ + + if( \gp\tool\RemoteGet::Test() === false ){ + return false; + } + + + if( !$this->FileSystem || !$this->FileSystem->ConnectOrPrompt('Admin/Permalinks') ){ + return false; + } + + return true; + } + + + /** + * Display instructions for manually creating the htaccess/web.config file + * + */ + public function ManualMethod(){ + global $langmessage, $dirPrefix; + + echo '

        '.$langmessage['manual_method'].'

        '; + echo '

        '; + echo str_replace('.htaccess',$this->rule_file_name,$langmessage['manual_htaccess']); + echo '

        '; + + //display rewrite code in textarea + $lines = explode("\n",$this->new_rules); + $len = 70; + foreach($lines as $line){ + $line_len = strlen($line)+(substr_count($line,"\t")*3); + $len = max($len,$line_len); + } + $len = min(140,$len); + echo ''; + echo '
        '; + echo ''; + echo '
        '; + + } + + + /** + * Save the htaccess rule to the server using $filesystem and test to make sure we aren't getting 500 errors + * + * @access public + * @since 1.7 + * + * @return boolean + */ + public function SaveRules(){ + global $langmessage, $dirPrefix; + + if( $this->new_rules === false ){ + return false; + } + + $filesystem_base = $this->FileSystem->get_base_dir(); + if( $filesystem_base === false ){ + return false; + } + + $filesystem_path = $filesystem_base.'/'.$this->rule_file_name; + + if( !$this->FileSystem->put_contents($filesystem_path,$this->new_rules) ){ + return false; + } + + + return $this->TestSave($filesystem_path); + } + + + /** + * Make sure the save hasn't broken the installation + * if TestResponse Fails, undo the changes + * + */ + public function TestSave($filesystem_path){ + + //only need to test if we might needt to undo + if( !$this->undo_if_failed ){ + return true; + } + + + if( !self::TestResponse($this->hide_index) ){ + + if( is_null($this->orig_rules) ){ + $this->FileSystem->unlink($filesystem_path); + }else{ + $this->FileSystem->put_contents($filesystem_path,$this->orig_rules); + } + return false; + } + + return true; + } + + + + /** + * Try to fetch a response using RemoteGet to see if we're getting a 500 error + * + * @access public + * @static + * @since 1.7 + * + * @return boolean + */ + public static function TestResponse($new_rewrite = true){ + + //get url, force gp_rewrite to $new_gp_rewrite + $rewrite_before = $_SERVER['gp_rewrite']; + $_SERVER['gp_rewrite'] = $new_rewrite; + \gp\tool::SetLinkPrefix(); + + + //without server name, we can't get a valid absoluteUrl + if( \gp\tool::ServerName() === false ){ + return false; + } + + + $abs_url = \gp\tool::AbsoluteUrl('Site_Map','',true,false); //can't be special_site_map, otherwise \gp\tool::IndexToTitle() will be called during install + $_SERVER['gp_rewrite'] = $rewrite_before; + \gp\tool::SetLinkPrefix(); + + + return self::ConfirmGet($abs_url, false); + } + + + /** + * Strip rules enclosed by comments + * + * @access public + * @static + * @since 1.7 + * + * @param string $contents .htaccess file contents + */ + public static function StripRules(&$contents){ + + //strip code + $pos = strpos($contents,'# BEGIN Typesetter'); + if( $pos === false ){ + return; + } + + $end_comment = '# END Typesetter'; + $pos2 = strpos($contents,$end_comment); + if( $pos2 > $pos ){ + $contents = substr_replace($contents,'',$pos,$pos2-$pos+strlen($end_comment)); + }else{ + $contents = substr($contents,0,$pos); + } + + $contents = rtrim($contents); + } + + + /** + * Return the .htaccess code that can be used to hide index.php + * add/remove cms rules from $original_contents to get new $contents + * + */ + public static function Rewrite_Rules( $hide_index = true, $home_root, $existing_contents = '', $www = null ){ + + $existing_contents = (string)$existing_contents; + + // IIS + if( self::IIS() ){ + return self::Rewrite_RulesIIS( $hide_index, $existing_contents ); + } + + if( self::Apache() ){ + return self::Rewrite_RulesApache($hide_index, $home_root, $existing_contents, $www); + } + + return false; + } + + + /** + * Generate rewrite rules for the apache server + * + */ + public static function Rewrite_RulesApache( $hide_index, $home_root, $contents, $www ){ + + // Apache + self::StripRules($contents); + + if( !$hide_index && is_null($www) ){ + return $contents; + } + + $home_root = rtrim($home_root,'/').'/'; + $new_lines = array(); + $server_name = \gp\tool::ServerName(true); + + + // with www + if( $www ){ + $new_lines[] = '# with www'; + $new_lines[] = 'RewriteCond %{HTTPS} off'; + $new_lines[] = 'RewriteCond %{HTTP_HOST} "^'.$server_name.'"'; + $new_lines[] = 'RewriteRule (.*) "http://www.'.$server_name.'/$1" [R=301,L]'; + + $new_lines[] = ''; + $new_lines[] = '# with www and https'; + $new_lines[] = 'RewriteCond %{HTTPS} on'; + $new_lines[] = 'RewriteCond %{HTTP_HOST} "^'.$server_name.'"'; + $new_lines[] = 'RewriteRule (.*) "https://www.'.$server_name.'/$1" [R=301,L]'; + + + // without www + }elseif( $www === false ){ + $new_lines[] = '# without www'; + $new_lines[] = 'RewriteCond %{HTTPS} off'; + $new_lines[] = 'RewriteCond %{HTTP_HOST} "^www.'.$server_name.'"'; + $new_lines[] = 'RewriteRule (.*) "http://'.$server_name.'/$1" [R=301,L]'; + + + $new_lines[] = ''; + $new_lines[] = '# without www and https'; + $new_lines[] = 'RewriteCond %{HTTPS} on'; + $new_lines[] = 'RewriteCond %{HTTP_HOST} "^www.'.$server_name.'"'; + $new_lines[] = 'RewriteRule (.*) "https://'.$server_name.'/$1" [R=301,L]'; + } + + $new_lines[] = "\n"; + + + // hide index.php + if( $hide_index ){ + $new_lines[] = 'RewriteBase "'.$home_root.'"'; + $new_lines[] = ''; + + $new_lines[] = '# Don\'t rewrite multiple times'; + $new_lines[] = 'RewriteCond %{QUERY_STRING} gp_rewrite'; + $new_lines[] = 'RewriteRule .* - [L]'; + $new_lines[] = ''; + + $new_lines[] = '# Redirect away from requests with index.php'; + $new_lines[] = 'RewriteRule index\.php(.*) "'.rtrim($home_root,'/').'$1" [R=302,L]'; + $new_lines[] = ''; + + $new_lines[] = '# Add gp_rewrite to root requests'; + $new_lines[] = 'RewriteRule ^$ "'.$home_root.'index.php?gp_rewrite" [qsa,L]'; + $new_lines[] = ''; + + $new_lines[] = '# Don\'t rewrite for static files'; + $new_lines[] = 'RewriteCond %{REQUEST_FILENAME} -f [OR]'; + $new_lines[] = 'RewriteCond %{REQUEST_FILENAME} -d [OR]'; + $new_lines[] = 'RewriteCond %{REQUEST_URI} \.(js|css|jpe?g|jpe|gif|png|ico)$ [NC]'; + $new_lines[] = 'RewriteRule .* - [L]'; + $new_lines[] = ''; + + $new_lines[] = '# Send all other requests to index.php'; + $new_lines[] = '# Append the gp_rewrite argument to tell cms not to use index.php and to prevent multiple rewrites'; + $new_lines[] = 'RewriteRule /?(.*) "'.$home_root.'index.php?gp_rewrite=$1" [qsa,L]'; + $new_lines[] = ''; + } + + + + return $contents.' + +# BEGIN Typesetter + + RewriteEngine On + + '.implode("\n\t",$new_lines).' + + + + + SetOutputFilter DEFLATE + + + +RewriteCond %{REQUEST_METHOD} ^TRACE +RewriteRule .* - [F] + +# END Typesetter'; + + } + + + /** + * Optimally, generating rules for IIS would involve parsing the xml and integrating cms rules + * + */ + public function Rewrite_RulesIIS( $hide_index = true, $existing_contents ){ + + + // anything less than 80 characters can be replaced safely + // other than that, users should handle manually + // Example empty configuration: < ?xml version="1.0" encoding="UTF-8" ? > + if( strlen($existing_contents) > 80 ){ + return false; + } + + if( !$hide_index ){ + return ' + +'; + } + + + + return ' + + + + + + + + + + + + + + + + + + + + + + + + + +'; + + } + + + /** + * Determine if installed on an IIS Server + * + */ + public static function IIS(){ + + if( !isset($_SERVER['SERVER_SOFTWARE']) ){ + return false; + } + + if( stripos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false ){ + return true; + } + return false; + } + + /** + * Determine if installed on an Apache Server + * + */ + public static function Apache(){ + + if( !isset($_SERVER['SERVER_SOFTWARE']) ){ + return false; + } + + if( stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== false ){ + return true; + } + return false; + } +} diff --git a/include/admin/Settings/Permissions.php b/include/admin/Settings/Permissions.php new file mode 100644 index 0000000..90716c7 --- /dev/null +++ b/include/admin/Settings/Permissions.php @@ -0,0 +1,147 @@ +'']; + + + public function __construct($args){ + parent::__construct($args); + } + + + + /** + * Display the permission options for a file + * + */ + public function DefaultDisplay(){ + global $langmessage; + + $indexes = $this->RequestedIndexes(); + if( !$indexes ){ + return; + } + + $count = count($indexes); + $first_index = $indexes[0]; + + + echo '
        '; + echo '
        '; + echo ''; + echo ''; + + + //heading + echo '

        '.\gp\tool::Link('Admin/Users',$langmessage['user_permissions']).' » '; + if( $count > 1 ){ + echo sprintf($langmessage['%s Pages'],$count); + }else{ + echo strip_tags(\gp\tool::GetLabelIndex($indexes[0])); + } + echo '

        '; + + + //list of files + if( $count > 1 ){ + $labels = array(); + foreach( $indexes as $index ){ + $labels[] = strip_tags(\gp\tool::GetLabelIndex($index)); + } + echo '

        '; + echo implode(', ',$labels); + echo '

        '; + } + + + //list of users + echo '
        '; + foreach($this->users as $username => $userinfo){ + $attr = ''; + if( $userinfo['editing'] == 'all' ){ + $attr = ' checked="checked"'; + }elseif( strpos($userinfo['editing'],','.$first_index.',') !== false ){ + $attr = ' checked="checked"'; + } + echo ' '; + } + echo '
        '; + + echo '

        '; + echo ''; + echo ' '; + echo '

        '; + + echo '
        '; + echo '
        '; + } + + + /** + * Save the permissions for a specific file + * + */ + public function SaveFilePermissions(){ + global $langmessage, $gp_index, $gpAdmin; + + $indexes = $this->RequestedIndexes(); + if( !$indexes ){ + return; + } + + + foreach($this->users as $username => $userinfo){ + + + // get array of editing indexes for the current user + $editing = explode(',',$userinfo['editing']); + + if( $userinfo['editing'] == 'all'){ + $editing = array_values($gp_index); + } + + $editing = array_intersect($gp_index,$editing); + $editing_before = $editing; + + + // add page index to user + if( isset($_POST['users'][$username]) ){ + $editing = array_merge($editing,$indexes); + + // remove page index from user + }else{ + $editing = array_diff($editing,$indexes); + + } + + $editing = array_intersect($gp_index,$editing); + + // don't save if there haven't been any changes + // keeps editing = all from being changed to editing = [list of all indexes] + if( $editing_before === $editing ){ + continue; + } + + + $editing_str = ''; + if( count($editing) ){ + $editing_str = ','.implode(',',$editing).','; + } + + $this->users[$username]['editing'] = $editing_str; + $this->UserFileDetails($username); + } + + return $this->SaveUserFile(false); + } + +} diff --git a/include/admin/Settings/Preferences.php b/include/admin/Settings/Preferences.php new file mode 100644 index 0000000..f08e8f6 --- /dev/null +++ b/include/admin/Settings/Preferences.php @@ -0,0 +1,269 @@ +page->ajaxReplace = []; + $this->username = $gpAdmin['username']; + if( !isset($this->users[$this->username]) ){ + msg($langmessage['OOPS']); + return; + } + + $this->user_info = $this->users[$this->username]; + $cmd = \gp\tool::GetCommand(); + + + switch($cmd){ + case 'changeprefs': + $this->DoChange(); + break; + case 'SaveGPUI': + $this->SaveGPUI(); + return; + } + + $this->Form(); + + } + + public function DoChange(){ + global $gpAdmin; + + $this->ChangeEmail(); + $this->ChangePass(); + + $this->SaveUserFile(); + } + + public function ChangeEmail(){ + global $langmessage; + + if( empty($_POST['email']) ){ + $this->users[$this->username]['email'] = ''; + return; + } + + if( $this->ValidEmail($_POST['email']) ){ + $this->users[$this->username]['email'] = $_POST['email']; + }else{ + msg($langmessage['invalid_email']); + } + + } + + public function ValidEmail($email){ + return (bool)preg_match('/^[^@]+@[^@]+\.[^@]+$/', $email); + } + + /** + * Save a user's new password + * + */ + public function ChangePass(){ + global $langmessage, $config; + + + $fields = 0; + if( !empty($_POST['oldpassword']) ){ + $fields++; + } + if( !empty($_POST['password']) ){ + $fields++; + } + if( !empty($_POST['password1']) ){ + $fields++; + } + if( $fields < 2 ){ + return; //assume user didn't try to reset password + } + + + //make sure password and password1 match + if( !$this->CheckPasswords() ){ + return false; + } + + //check the old password + $passed = false; + $pass_hash = \gp\tool\Session::PassAlgo($this->user_info); + + if( $pass_hash == 'password_hash' ){ + $pass_sha512 = \gp\tool::hash($_POST['oldpassword'], 'sha512', 50); + $passed = password_verify($pass_sha512, $this->user_info['password']); + }else{ + $oldpass = \gp\tool::hash($_POST['oldpassword'], $pass_hash); + $passed = $this->user_info['password'] == $oldpass; + } + + if( !$passed ){ + msg($langmessage['couldnt_reset_pass']); + return false; + } + + $this->SetUserPass( $this->username, $_POST['password']); + } + + + public function Form(){ + global $langmessage, $gpAdmin; + + if( $_SERVER['REQUEST_METHOD'] == 'POST'){ + $array = $_POST; + }else{ + $array = $this->user_info + $gpAdmin; + } + $array += array('email'=>''); + + echo '

        '.$langmessage['Preferences'].'

        '; + + echo '
        '; + echo ''; + echo ''; + + + //email + echo ''; + + + + echo ''; + + echo ''; + + echo ''; + + echo ''; + + $this->AlgoSelect(); + + echo '
        '.$langmessage['general_settings'].'
        '; + echo $langmessage['email_address']; + echo ''; + echo ''; + echo '
        '.$langmessage['change_password'].'
        '; + echo $langmessage['old_password']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['new_password']; + echo ''; + echo ''; + echo '
        '; + echo $langmessage['repeat_password']; + echo ''; + echo ''; + echo '
        '; + + echo '
        '; + echo ''; + echo ' '; + echo ' '; + echo '
        '; + + echo '

        '; + echo ''; + echo $langmessage['see_also']; + echo ' '; + echo \gp\tool::Link('Admin_Configuration',$langmessage['configuration'],'','data-cmd="gpabox"'); + echo '

        '; + + echo '
  • '; + echo ''; + + } + + + + /** + * Save UI values for the current user + * + */ + public static function SaveGPUI(){ + global $gpAdmin; + + $possible = array(); + + $possible['gpui_cmpct'] = 'integer'; + $possible['gpui_vis'] = [ + 'con' => 'con', + 'cur' => 'cur', + 'app' => 'app', + 'add' => 'add', + 'set' => 'set', + 'use' => 'use', + 'cms' => 'cms', + 'res' => 'res', + 'tool' => 'tool', + 'notifications' => 'notifications', // since 5.2 + 'false' => false, + ]; + $possible['gpui_tx'] = 'integer'; + $possible['gpui_ty'] = 'integer'; + $possible['gpui_ckx'] = 'integer'; + $possible['gpui_cky'] = 'integer'; + $possible['gpui_exp'] = 'integer'; // editor expanded, since 5.2 + $possible['gpui_thw'] = 'integer'; + + foreach($possible as $key => $key_possible){ + + if( !isset($_POST[$key]) ){ + continue; + } + $value = $_POST[$key]; + + if( $key_possible == 'boolean' ){ + if( !$value || $value === 'false' ){ + $value = false; + }else{ + $value = true; + } + }elseif( $key_possible == 'integer' ){ + $value = (int)$value; + }elseif( is_array($key_possible) ){ + if( !isset($key_possible[$value]) ){ + continue; + } + } + + $gpAdmin[$key] = $value; + } + + //remove gpui_ settings no longer in $possible + unset( + $gpAdmin['gpui_pdock'], + $gpAdmin['gpui_con'], + $gpAdmin['gpui_cur'], + $gpAdmin['gpui_app'], + $gpAdmin['gpui_add'], + $gpAdmin['gpui_set'], + $gpAdmin['gpui_upd'], + $gpAdmin['gpui_use'], + $gpAdmin['gpui_edb'], + $gpAdmin['gpui_brdis'], // 3.5 + $gpAdmin['gpui_ctx'] // 5.0 + ); + + //send response so an error is not thrown + echo \gp\tool\Output\Ajax::Callback().'([]);'; + die(); + } + } + +} + +namespace{ + class admin_preferences extends \gp\admin\Settings\Preferences{} +} diff --git a/include/admin/Settings/Users.php b/include/admin/Settings/Users.php new file mode 100644 index 0000000..8af6ba1 --- /dev/null +++ b/include/admin/Settings/Users.php @@ -0,0 +1,785 @@ + '', + 'ChangePass' => '', + 'Details' => 'ChangeDetails', + ]; + + protected $cmds_post = [ + 'CreateNewUser' => 'NewUserForm', + 'RemoveUser' => 'DefaultDisplay', + 'ResetPass' => 'ChangePass', + 'SaveChanges' => 'ChangeDetails', + ]; + + + public function __construct($args){ + global $langmessage; + + parent::__construct($args); + + $this->page->head_js[] = '/include/js/admin_users.js'; + $this->possible_permissions = $this->PossiblePermissions(); + $this->GetUsers(); + + } + + + /** + * Return an array of possible permissions + * + */ + public static function PossiblePermissions(){ + $possible = array(); + $scripts = \gp\admin\Tools::AdminScripts(); + + foreach($scripts as $script => $info){ + + if( isset($info['permission']) ){ + continue; + } + + if( !isset($info['label']) ){ + continue; + } + $script = str_replace('/','_',$script); + $possible[$script] = $info['label']; + } + + return $possible; + } + + + /** + * Save changes made to an existing user's permissions + * + */ + public function SaveChanges(){ + global $langmessage,$gpAdmin; + + $username =& $_REQUEST['username']; + if( !isset($this->users[$username]) ){ + msg($langmessage['OOPS']); + return false; + } + + if( !empty($_POST['email']) ){ + $this->users[$username]['email'] = $_POST['email']; + } + + $this->users[$username]['granted'] = $this->GetPostedPermissions($username); + $this->users[$username]['editing'] = $this->GetEditingPermissions(); + + //this needs to happen before SaveUserFile(); + //update the /_session file + $userinfo =& $this->users[$username]; + $userinfo = \gp\tool\Session::SetSessionFileName($userinfo,$username); //make sure $userinfo['file_name'] is set + + + if( !$this->SaveUserFile() ){ + return false; + } + + // update the $user_file_name file + $this->UserFileDetails($username); + return true; + } + + /** + * Update the users session file with new permission data + * + */ + public function UserFileDetails($username){ + global $dataDir, $gpAdmin; + + $user_info = $this->users[$username]; + $user_file = $dataDir.'/data/_sessions/'.$user_info['file_name']; + + if( $gpAdmin['username'] === $username ){ + $new_info =& $gpAdmin; + }else{ + $new_info = \gp\tool\Files::Get($user_file,'gpAdmin'); + } + + if( !$new_info ){ + return; + } + + $new_info['granted'] = $user_info['granted']; + $new_info['editing'] = $user_info['editing']; + \gp\tool\Files::SaveData($user_file,'gpAdmin',$new_info); + } + + /** + * Display the permissions of a single user + * + */ + public function ChangeDetails(){ + global $langmessage; + + $username =& $_REQUEST['username']; + if( !isset($this->users[$username]) ){ + msg($langmessage['OOPS']); + return false; + } + + echo '

    '.$langmessage['user_permissions'].'

    '; + + $userinfo = $this->users[$username]; + + echo '
    '; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + $this->DetailsForm($userinfo); + + echo ''; + echo ''; + + echo '
    '; + echo $langmessage['details']; + echo ' - '; + echo $username; + echo '
    '; + echo ''; + echo ' '; + echo ' '; + echo ' '; + echo '
    '; + echo '
    '; + + } + + /** + * Remove a user from the installation + * + */ + public function RemoveUser(){ + global $langmessage; + $username = $this->CheckUser(); + + if( $username == false ){ + return; + } + + unset($this->users[$username]); + return $this->SaveUserFile(); + } + + /** + * Make sure the submitted username exists + * + */ + public function CheckUser(){ + global $langmessage,$gpAdmin; + $username = $_POST['username']; + + if( !isset($this->users[$username]) ){ + msg($langmessage['OOPS']); + return false; + } + + //don't allow deleting self + if( $username == $gpAdmin['username'] ){ + msg($langmessage['OOPS']); + return false; + } + return $username; + } + + + + public function CreateNewUser(){ + global $langmessage; + $_POST += array('grant'=>''); + + if( ($_POST['password']=="") || ($_POST['password'] !== $_POST['password1']) ){ + msg($langmessage['invalid_password']); + return false; + } + + + $newname = $_POST['username']; + $test = str_replace( array('.','_'), array(''), $newname ); + if( empty($test) || !ctype_alnum($test) ){ + msg($langmessage['invalid_username']); + return false; + } + + if( isset($this->users[$newname]) ){ + msg($langmessage['OOPS']); + return false; + } + + + if( !empty($_POST['email']) ){ + $this->users[$newname]['email'] = $_POST['email']; + } + + $this->users[$newname]['granted'] = $this->GetPostedPermissions($newname); + $this->users[$newname]['editing'] = $this->GetEditingPermissions(); + + $this->SetUserPass( $newname, $_POST['password']); + + if( $this->SaveUserFile() ){ + $url = \gp\tool::GetUrl('Admin/Users','',false); + \gp\tool::Redirect($url); + } + + } + + + /** + * Set the user password and password hash algorithm + * + */ + public function SetUserPass( $username, $password ){ + + $user_info =& $this->users[$username]; + + if( function_exists('password_hash') && $_REQUEST['algo'] == 'password_hash' ){ + $temp = \gp\tool::hash($password,'sha512',50); + $user_info['password'] = password_hash($temp,PASSWORD_DEFAULT); + $user_info['passhash'] = 'password_hash'; + + }else{ + $user_info['password'] = \gp\tool::hash($password,'sha512'); + $user_info['passhash'] = 'sha512'; + } + + } + + + /** + * Return the posted admin permissions + * + */ + public function GetPostedPermissions($username){ + global $gpAdmin; + + if( isset($_POST['grant_all']) && ($_POST['grant_all'] == 'all') ){ + return 'all'; + } + + $_POST += array('grant'=>array()); + $array = $_POST['grant']; + + //cannot remove self from Admin/Users + if( $username == $gpAdmin['username'] ){ + $array = array_merge($array, array('Admin/Users')); + } + + if( !is_array($array) ){ + return ''; + } + + $keys = array_keys($this->possible_permissions); + $array = array_intersect($keys,$array); + return implode(',',$array); + } + + + /** + * Return the posted file editing permissions + * + */ + public function GetEditingPermissions(){ + global $gp_titles; + + if( isset($_POST['editing_all']) && ($_POST['editing_all'] == 'all') ){ + return 'all'; + } + + $_POST += array('titles'=>array()); + $array = $_POST['titles']; + if( !is_array($array) ){ + return ''; + } + + $keys = array_keys($gp_titles); + $array = array_intersect($keys,$array); + if( count($array) > 0 ){ + return ','.implode(',',$array).','; + } + return ''; + } + + + + public function SaveUserFile($refresh = true ){ + global $langmessage; + + if( !\gp\tool\Files::SaveData('_site/users','users',$this->users) ){ + msg($langmessage['OOPS']); + return false; + } + + if( $refresh && isset($_GET['gpreq']) && $_GET['gpreq'] == 'json' ){ + msg($langmessage['SAVED'].' '.$langmessage['REFRESH']); + }else{ + msg($langmessage['SAVED']); + } + return true; + } + + + /** + * Show all users and their permissions + * + */ + public function DefaultDisplay(){ + global $langmessage; + + + echo '

    '.$langmessage['user_permissions'].'

    '; + + ob_start(); + echo ''; + echo ''; + + foreach($this->users as $username => $userinfo){ + + echo ''; + + //file editing + echo ''; + + //options + echo ''; + echo ''; + } + echo ''; + + echo '
    '; + echo $langmessage['username']; + echo ''; + echo $langmessage['Password Algorithm']; + echo ''; + echo $langmessage['permissions']; + echo ''; + echo $langmessage['file_editing']; + echo ''; + echo $langmessage['options']; + echo '
    '; + echo $username; + + //algorithm + echo ''; + $this->PassAlgo($userinfo); + + + //admin permissions + echo ''; + if( $userinfo['granted'] == 'all' ){ + echo 'all'; + }elseif( !empty($userinfo['granted']) ){ + + $permissions = explode(',',$userinfo['granted']); + $list = array(); + foreach($permissions as $permission){ + if( isset($this->possible_permissions[$permission]) ){ + $list[] = $this->possible_permissions[$permission]; + } + } + if( count($list) ){ + echo implode(', ',$list); + }else{ + echo $langmessage['None']; + } + }else{ + echo $langmessage['None']; + } + + echo ''; + + if( $userinfo['editing'] == 'all' ){ + echo $langmessage['All']; + }else{ + + $count = preg_match_all('#,#',$userinfo['editing']) - 1; //count the commas + if( $count > 0 ){ + echo sprintf($langmessage['%s Pages'],$count); + }else{ + echo $langmessage['None']; + } + } + + echo ''; + echo \gp\tool::Link('Admin/Users',$langmessage['details'],'cmd=details&username='.$username); + echo '   '; + echo \gp\tool::Link('Admin/Users',$langmessage['password'],'cmd=changepass&username='.$username); + echo '   '; + + $title = sprintf($langmessage['generic_delete_confirm'],htmlspecialchars($username)); + echo \gp\tool::Link('Admin/Users',$langmessage['delete'],'cmd=RemoveUser&username='.$username,array('data-cmd'=>'postlink','title'=>$title,'class'=>'gpconfirm')); + echo '
    '; + echo \gp\tool::Link('Admin/Users',$langmessage['new_user'],'cmd=newuserform'); + echo '
    '; + + $content = ob_get_clean(); + + if( $this->has_weak_pass ){ + echo '

    Warning: '; + echo 'Weak password algorithms are being used for one or more users. To fix this issue, reset the user\'s password. '; + echo '

    '; + } + + echo $content; + } + + + /** + * Display the password algorithm being used for the user + * + */ + public function PassAlgo($userinfo){ + + $algo = \gp\tool\Session::PassAlgo($userinfo); + switch($algo){ + case 'md5': + case 'sha1': + $this->has_weak_pass = true; + echo ''.$algo.''; + return; + } + echo $algo; + } + + + /** + * Display form for adding new admin user + * + */ + public function NewUserForm(){ + global $langmessage; + + echo '

    '.$langmessage['user_permissions'].'

    '; + + $_POST += array('username'=>'','email'=>'','grant'=>array(),'grant_all'=>'all','editing_all'=>'all'); + + echo '
    '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + $this->AlgoSelect(); + + $_POST['granted'] = $this->GetPostedPermissions(false); + $_POST['editing'] = $this->GetEditingPermissions(); + $this->DetailsForm($_POST); + + + echo ''; + + echo '
    '; + echo $langmessage['new_user']; + echo '
    '; + echo $langmessage['username']; + echo ''; + echo ''; + echo '
    '; + echo $langmessage['password']; + echo ''; + echo ''; + echo '
    '; + echo str_replace(' ',' ',$langmessage['repeat_password']); + echo ''; + echo ''; + echo '
    '; + echo ''; + echo ''; + echo ' '; + echo ' '; + echo ' '; + echo '
    '; + echo '
    '; + } + + /** + * Display '; + foreach($algos as $algo => $avail){ + + $attr = ''; + if( !$avail ){ + $attr .= 'disabled'; + } + if( isset($_REQUEST['algo']) && $algo == $_REQUEST['algo'] ){ + $attr .= ' selected'; + } + echo ''; + } + echo ''; + + echo '   password_hash requires PHP 5.5+'; + + echo ''; + + + } + + + /** + * Display permission options + * + */ + public function DetailsForm( $values=array() ){ + global $langmessage, $gp_titles; + + $values += array('granted'=>'','email'=>''); + + //email address + echo ''; + echo str_replace(' ',' ',$langmessage['email_address']); + echo ''; + echo ''; + echo ''; + + + //admin permissions + echo ''; + echo str_replace(' ',' ',$langmessage['grant_usage']); + echo ''; + + $all = false; + $current = $values['granted']; + $checked = ''; + if( $current == 'all' ){ + $all = true; + $checked = ' checked="checked" '; + }else{ + $current = ','.$current.','; + } + + echo '

    '; + + foreach($this->possible_permissions as $permission => $label){ + $checked = ''; + if( $all ){ + $checked = ' checked="checked" '; + }elseif( strpos($current,','.$permission.',') !== false ){ + $checked = ' checked="checked" '; + } + + echo ' '; + } + + echo ''; + + //file editing + echo ''; + echo $langmessage['file_editing']; + echo ''; + + $editing_values = $values['editing']; + $all = ($editing_values == 'all'); + $checked = $all ? ' checked="checked" ' : ''; + echo '

    '; + + echo '
    '; + + $ordered = array(); + foreach($gp_titles as $index => $info){ + $ordered[$index] = strip_tags(\gp\tool::GetLabelIndex($index)); + } + + uasort($ordered,'strnatcasecmp'); + + foreach($ordered as $index => $label){ + $label = strip_tags($label); + $checked = ''; + if( $all ){ + $checked = ' checked="checked" '; + }elseif( strpos($editing_values,','.$index.',') !== false ){ + $checked = ' checked="checked" '; + } + + echo ' '; + } + + echo '
    '; + echo ''; + + } + + /** + * Display form for changing a user password + * + */ + public function ChangePass(){ + global $langmessage; + + $username =& $_REQUEST['username']; + if( !isset($this->users[$username]) ){ + msg($langmessage['OOPS']); + return; + } + + + echo '
    '; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + $this->AlgoSelect(); + + echo ''; + echo '
    '; + echo $langmessage['change_password']; + echo ' - '; + echo $username; + echo '
    '; + echo $langmessage['new_password']; + echo ''; + echo ''; + echo '
    '; + echo str_replace(' ',' ',$langmessage['repeat_password']); + echo ''; + echo ''; + echo '
    '; + echo ''; + echo ''; + echo ' '; + echo '
    '; + echo '
    '; + } + + /** + * Save a user's new password + * + */ + public function ResetPass(){ + global $langmessage, $config; + + if( !$this->CheckPasswords() ){ + return false; + } + + $username = $_POST['username']; + if( !isset($this->users[$username]) ){ + msg($langmessage['OOPS']); + return false; + } + + $this->SetUserPass( $username, $_POST['password']); + + return $this->SaveUserFile(); + } + + /** + * Check the posted passwords + * Make sure they're not empty and the match each other + * + */ + public function CheckPasswords(){ + global $langmessage; + + //see also Admin/Users for password checking + if( ($_POST['password']=="") || ($_POST['password'] !== $_POST['password1']) ){ + msg($langmessage['invalid_password']); + return false; + } + return true; + } + + public function GetUsers(){ + + $this->users = \gp\tool\Files::Get('_site/users'); + + //fix the editing value + foreach($this->users as $username => $userinfo){ + $userinfo += array('granted'=>''); + \gp\admin\Tools::EditingValue($userinfo); + $this->users[$username] = $userinfo; + } + } + + + /** + * Get the menu indexes + * + */ + public function RequestedIndexes(){ + global $langmessage, $gp_titles; + + $_REQUEST += array('index'=>''); + $indexes = explode(',',$_REQUEST['index']); + + if( empty($indexes) ){ + msg($langmessage['OOPS'].' Invalid Title (1)'); + return; + } + + $cleaned = array(); + foreach($indexes as $index){ + if( !isset($gp_titles[$index]) ){ + continue; + } + $cleaned[] = $index; + } + + if( empty($cleaned) ){ + msg($langmessage['OOPS'].' Invalid Title (2)'); + return; + } + + return $cleaned; + } + +} diff --git a/include/admin/Tools.php b/include/admin/Tools.php new file mode 100644 index 0000000..1a90a4a --- /dev/null +++ b/include/admin/Tools.php @@ -0,0 +1,1843 @@ + []]; + + return \gp\tool\Files::$last_modified; + } + + + public static function CheckArray($array, $update_data){ + + foreach($array as $addon => $addon_info){ + + $addon_id = false; + if( isset($addon_info['id']) ){ + $addon_id = $addon_info['id']; + }elseif( isset($addon_info['addon_id']) ){ //for layouts + $addon_id = $addon_info['addon_id']; + } + + if( !$addon_id || !isset($update_data['packages'][$addon_id]) ){ + continue; + } + + $installed_version = 0; + if( isset($addon_info['version']) ){ + $installed_version = $addon_info['version']; + } + + $new_addon_info = $update_data['packages'][$addon_id]; + $new_addon_version = $new_addon_info['version']; + if( version_compare($installed_version, $new_addon_version, '>=') ){ + continue; + } + + //new version found + if( !isset($new_addon_info['name']) && isset($addon_info['name']) ){ + $new_addon_info['name'] = $addon_info['name']; + } + self::$new_versions[$addon_id] = $new_addon_info; + } + + } + + + public static function AdminScripts(){ + global $langmessage, $config; + $scripts = []; + + // Content + $scripts['Admin/Menu'] = [ + 'class' => '\\gp\\admin\\Menu', + 'method' => 'RunScript', + 'label' => $langmessage['file_manager'], + 'group' => 'content', + ]; + + $scripts['Admin/Menu/Menus'] = [ + 'class' => '\\gp\\admin\\Menu\\Menus', + 'method' => 'RunScript', + ]; + + $scripts['Admin/Menu/Ajax'] = [ + 'class' => '\\gp\\admin\\Menu\\Ajax', + 'method' => 'RunScript', + ]; + + $scripts['Admin/Uploaded'] = [ + 'class' => '\\gp\\admin\\Content\\Uploaded', + 'method' => 'RunScript', + 'label' => $langmessage['uploaded_files'], + 'group' => 'content', + ]; + + $scripts['Admin/Extra'] = [ + 'class' => '\\gp\\admin\\Content\\Extra', + 'method' => 'RunScript', + 'label' => $langmessage['theme_content'], + 'group' => 'content', + ]; + + $scripts['Admin/Galleries'] = [ + 'class' => '\\gp\\admin\\Content\\Galleries', + 'label' => $langmessage['galleries'], + 'group' => 'content', + ]; + + $scripts['Admin/Trash'] = [ + 'class' => '\\gp\\admin\\Content\\Trash', + 'label' => $langmessage['trash'], + 'group' => 'content', + ]; + + // Appearance + $scripts['Admin_Theme_Content'] = [ + 'class' => '\\gp\\admin\\Layout', + 'method' => 'RunScript', + 'label' => $langmessage['Appearance'], + 'group' => 'appearance', + ]; + + $scripts['Admin_Theme_Content/Edit'] = [ + 'class' => '\\gp\\admin\\Layout\\Edit', + 'method' => 'RunScript', + 'label' => $langmessage['layouts'], + ]; + + $scripts['Admin_Theme_Content/Available'] = [ + 'class' => '\\gp\\admin\\Layout\\Available', + 'method' => 'ShowAvailable', + 'label' => $langmessage['Available'] . ' (' . $langmessage['layouts'] . ')', + ]; + + $scripts['Admin_Theme_Content/Text'] = [ + 'class' => '\\gp\\admin\\Layout\\Text', + 'method' => 'RunScript', + ]; + + $scripts['Admin_Theme_Content/Image'] = [ + 'class' => '\\gp\\admin\\Layout\\Image', + 'method' => 'RunScript', + ]; + + if( gp_remote_themes ){ + $scripts['Admin_Theme_Content/Remote'] = [ + 'class' => '\\gp\\admin\\Layout\\Remote', + 'method' => 'DefaultDisplay', + 'label' => $langmessage['Search'] . ' (' . $langmessage['layouts'] . ')', + ]; + } + + // Settings + $scripts['Admin/Configuration'] = [ + 'class' => '\\gp\\admin\\Configuration', + 'method' => 'RunScript', + 'label' => $langmessage['configuration'], + 'group' => 'settings', + ]; + + $scripts['Admin/Configuration/CDN'] = [ + 'class' => '\\gp\\admin\\Configuration\\CDN', + 'method' => 'RunScript', + 'label' => 'CDN', + 'group' => 'settings', + ]; + + $scripts['Admin/Users'] = [ + 'class' => '\\gp\\admin\\Settings\\Users', + 'method' => 'RunScript', + 'label' => $langmessage['user_permissions'], + 'group' => 'settings', + ]; + + $scripts['Admin/Permissions'] = [ + 'class' => '\\gp\\admin\\Settings\\Permissions', + 'method' => 'RunScript', + ]; + + $scripts['Admin/CKEditor'] = [ + 'class' => '\\gp\\admin\\Settings\\CKEditor', + 'label' => 'CKEditor', + 'group' => 'settings', + ]; + + $scripts['Admin/Classes'] = [ + 'class' => '\\gp\\admin\\Settings\\Classes', + 'label' => $langmessage['Manage Classes'], + 'group' => 'settings', + ]; + + $scripts['Admin/Permalinks'] = [ + 'class' => '\\gp\\admin\\Settings\\Permalinks', + 'label' => $langmessage['permalinks'], + 'group' => 'settings', + ]; + + $scripts['Admin/Missing'] = [ + 'class' => '\\gp\\admin\\Settings\\Missing', + 'method' => 'RunScript', + 'label' => $langmessage['Link Errors'], + 'group' => 'settings', + ]; + + // Tools + $scripts['Admin/Port'] = [ + 'class' => '\\gp\\admin\\Tools\\Port', + 'label' => $langmessage['Export'], + 'group' => 'tools', + 'method' => 'RunScript', + ]; + + $scripts['Admin/Status'] = [ + 'class' => '\\gp\\admin\\Tools\\Status', + 'label' => $langmessage['Site Status'], + 'group' => 'tools', + 'method' => 'RunScript', + ]; + + $scripts['Admin/Uninstall'] = [ + 'class' => '\\gp\\admin\\Tools\\Uninstall', + 'label' => $langmessage['uninstall_prep'], + 'group' => 'tools', + ]; + + $scripts['Admin/Cache'] = [ + 'class' => '\\gp\\admin\\Tools\\Cache', + 'label' => $langmessage['Resource Cache'], + 'group' => 'tools', + ]; + + // Unlisted + $scripts['Admin/Addons'] = [ + 'class' => '\\gp\\admin\\Addons', + 'method' => 'RunScript', + 'label' => $langmessage['plugins'], + ]; + + $scripts['Admin/Addons/Available'] = [ + 'class' => '\\gp\\admin\\Addon\\Available', + 'method' => 'DefaultDisplay', + 'label' => $langmessage['Available'] . ' (' . $langmessage['plugins'] . ')', + ]; + + if( gp_remote_plugins ){ + $scripts['Admin/Addons/Remote'] = [ + 'class' => '\\gp\\admin\\Addon\\Remote', + 'method' => 'DefaultDisplay', + 'label' => $langmessage['Search'] . ' (' . $langmessage['plugins'] . ')', + ]; + } + + $scripts['Admin/Errors'] = [ + 'class' => '\\gp\\admin\\Tools\\Errors', + 'label' => 'Errors', + ]; + + $scripts['Admin/About'] = [ + 'class' => '\\gp\\admin\\About', + 'label' => 'About ' . CMS_NAME, + ]; + + $scripts['Admin/Browser'] = [ + 'class' => '\\gp\\admin\\Content\\Browser', + 'permission' => 'Admin_Uploaded', + ]; + + $scripts['Admin/Preferences'] = [ + 'class' => '\\gp\\admin\\Settings\\Preferences', + 'label' => $langmessage['Preferences'], + ]; + + $scripts['Admin/Notifications'] = [ + 'class' => '\\gp\\admin\\Notifications', + 'method' => 'ListNotifications', + 'label' => $langmessage['Notifications'], + ]; + + $scripts['Admin/Notifications/Manage'] = [ + 'class' => '\\gp\\admin\\Notifications', + 'method' => 'ManageNotifications', + 'permission' => 'Admin/Notifications', + ]; + + $scripts['Admin/Revisions'] = [ + 'class' => '\\gp\\admin\\Content\\Revisions', + 'method' => 'RunScript', + ]; + + // Addon admin links + if( isset($config['admin_links']) && is_array($config['admin_links']) ){ + foreach( $config['admin_links'] as $link_name => $addon_info ){ + $link_label = $addon_info['label']; + $link_label = \gp\tool\Plugins::Filter('AdminLinkLabel', [$link_label, $link_name]); + $config['admin_links'][$link_name]['label'] = $link_label; + } + + //prefix admin link labels with plugin icon + $admin_links = $config['admin_links']; + foreach( $admin_links as $link_name => $addon_info ){ + $addon_name = $config['addons'][$addon_info['addon']]['name']; + $admin_links[$link_name]['label'] = ' ' . $addon_info['label']; + } + $scripts += $admin_links; + } + + gpSettingsOverride('admin_scripts', $scripts); + + return $scripts; + } + + + /** + * Determine if the current user has permissions for the $script + * @static + * @return bool + * + */ + public static function HasPermission($script){ + global $gpAdmin; + if( is_array($gpAdmin) ){ + $gpAdmin += ['granted' => '']; + return self::CheckPermission($gpAdmin['granted'], $script); + } + return false; + } + + + /** + * Determine if a user has permissions for the $script + * @static + * @since 3.0b2 + * @return bool + * + */ + public static function CheckPermission($granted,$script){ + + if( $granted == 'all' ){ + return true; + } + + $script = self::WhichPermission($script); + $granted = ',' . $granted . ','; + if( strpos($granted, ',' . $script . ',') !== false ){ + return true; + } + + return false; + } + + + /** + * Return the permission setting that should be checked against a list of grated permissions + * Admin_Browser -> Admin_Uploaded + * Admin_Theme_Content/Text -> Admin_Theme_Content + * + */ + public static function WhichPermission($script){ + + // prepare list of permissions + $scripts = self::AdminScripts(); + $possible = []; + foreach($scripts as $pscript => $info){ + $pscript = str_replace('/', '_', $pscript); + if( isset($info['permission']) ){ + $possible[$pscript] = $info['permission']; + + }elseif( isset($info['label']) ){ + $possible[$pscript] = $pscript; + } + } + + // find the relevant permission in the list of possible permissions + $script = str_replace('/', '_', $script); + $parts = explode('_', $script); + + while($parts){ + $check = implode('_', $parts); + if( !isset($possible[$check]) ){ + array_pop($parts); + continue; + } + return $possible[$check]; + } + + return $script; + } + + + /** + * Determine if a user can edit a specific page + * @static + * @since 3.0b2 + * @param string $index The data index of the page + * @return bool + * + */ + public static function CanEdit($index){ + global $gpAdmin; + + //pre 3.0 check + if( !isset($gpAdmin['editing']) ){ + return self::HasPermission('file_editing'); + } + + if( $gpAdmin['editing'] == 'all' ){ + return true; + } + + if( strpos($gpAdmin['editing'], ',' . $index . ',') !== false ){ + return true; + } + + return false; + } + + + + /** + * Used to update the basic 'file_editing' permission value to the new 'editing' value used in 3.0b2+ + * @since 3.0b2 + * @static + * + */ + public static function EditingValue(&$user_info){ + if( isset($user_info['editing']) ){ + return; + } + if( self::CheckPermission($user_info['granted'], 'file_editing') ){ + $user_info['editing'] = 'all'; + return 'all'; + } + $user_info['editing'] = ''; + } + + + + /** + * Output the main admin toolbar + * @static + * + */ + public static function GetAdminPanel(){ + global $page, $gpAdmin; + + //don't send the panel when it's a gpreq=json request + if( !self::$show_toolbar ){ + return; + } + + + \gp\tool::LoadComponents('gp-admin-toolbar'); + + $reqtype = \gp\tool::RequestType(); + if( $reqtype != 'template' && $reqtype != 'admin' ){ + return; + } + + $class = ''; + $position = ''; + + if( \gp\tool::RequestType() != 'admin' ){ + $position = ' style="top:' . max(-10, $gpAdmin['gpui_ty']) . 'px;' + . 'left:' . max(-10, $gpAdmin['gpui_tx']) . 'px"'; + if( isset($gpAdmin['gpui_cmpct']) && $gpAdmin['gpui_cmpct'] ){ + $class = ' compact'; + if( $gpAdmin['gpui_cmpct'] === 2 ){ + $class = ' compact min'; + }elseif( $gpAdmin['gpui_cmpct'] === 3 ){ + $class = ' minb'; + } + } + } + + $class = ' class="keep_viewable' . $class.'"'; + + echo "\n\n"; + echo '
    '; + + //toolbar + echo '
    '; + echo ''; + + echo \gp\tool::Link( + '', + '' + ); + + echo \gp\tool::Link( + 'Admin', + '' + ); + + echo \gp\tool::Link( + 'special_gpsearch', + '', + '', + ['data-cmd' => 'gpabox'] + ); + + echo '
    '; + + self::AdminPanelLinks(true); + + echo '
    '; //end simplepanel + + echo "\n\n"; + + self::AdminToolbar(); + } + + + /** + * Show Admin Toolbar + * + */ + public static function AdminToolbar(){ + global $page, $langmessage; + + if( !method_exists($page, 'AdminLinks') ){ + return; + } + + if( isset($GLOBALS['GP_ARRANGE_CONTENT']) ){ + return; + } + + $links = $page->AdminLinks(); + + if( empty($links) ){ + return; + } + + echo '
    '; + echo ''; + + self::ToolbarEditLinks(); + echo '
    '; + } + + + /** + * Toolbar edit links + * + */ + public static function ToolbarEditLinks(){ + global $page, $gp_titles, $langmessage; + + if( !\gp\admin\Tools::CanEdit($page->gp_index) ){ + return; + } + + $links = []; + + //page edit + if( $page->pagetype == 'display' ){ + $links[] = \gp\tool::Link( + $page->title, + ' ' . $langmessage['Sections'], + 'cmd=ManageSections', + [ + 'data-cmd' => 'inline_edit_generic', + 'data-arg' => 'manage_sections' + ] + ); + } + + //extra edit + $links[] = \gp\tool::Link( + $page->title, + ' ' . $langmessage['theme_content'], + 'cmd=ManageSections&mode=extra', + [ + 'data-cmd' => 'inline_edit_generic', + 'data-arg' => 'manage_sections', + 'data-mode' => 'extra', + 'class' => 'gp_extra_edit' + ] + ); + + //layout edit + $links[] = \gp\tool::Link( + 'Admin_Theme_Content/Edit/' . urlencode( \gp\tool::GetCurrentLayoutId() ), + ' ' . $langmessage['layout'], + 'redir=' . rawurlencode($page->requested) + ); + + //revision history + $links[] = \gp\tool::Link( + '/Admin/Revisions/'.$page->gp_index, + ' ' . $langmessage['Revision History'], + '', + [ + 'title' => $langmessage['Revision History'], + 'class' => 'admin-link admin-link-revision-history', + ] + ); + + echo '
      '; + + self::FormatAdminLinks($links); + + echo '
    '; + } + + + public static function FormatAdminLinks($links){ + global $langmessage; + + foreach($links as $label => $link){ + echo '
  • '; + + if( is_numeric($label) ){ + if( is_array($link) ){ + echo call_user_func_array(['\\gp\\tool', 'Link'], $link); /* preferred */ + }else{ + echo $link; //just a text label + } + echo '
  • '; + continue; + } + + if( empty($link) ){ + echo ''; + echo $label; + echo ''; + }elseif( is_array($link) ){ + $add_class = ($label == $langmessage['options']) ? ' admin-link-dropdown-options' : ''; + echo ''; + echo ' ' . $label; + echo ''; + echo '
      '; + self::FormatAdminLinks($link); + echo '
    '; + }else{ + echo ''; + echo $label; + echo ''; + } + + echo '
  • '; + } + } + + + /** + * Output the link areas that are displayed in the main admin toolbar and admin_main + * @param bool $in_panel Whether or not the links will be displayed in the toolbar + * @static + * + */ + public static function AdminPanelLinks($in_panel=true){ + global $langmessage, $page, $gpAdmin; + + //content + $links = self::GetAdminGroup('content'); + self::_AdminPanelLinks($in_panel, $links, 'Content', 'fa fa-file-text', 'con'); + + //appearance + $links = self::GetAppearanceGroup($in_panel); + self::_AdminPanelLinks($in_panel, $links, 'Appearance', 'fa fa-th', 'app'); + + //add-ons + $addon_links = self::GetAddonLinks($in_panel); // now returns array( (string)links, (boolean)permissions ) + $links = $addon_links[0]; + $addon_permissions = $addon_links[1]; + // msg("Any Addon Permisisons? " . pre($addon_permissions) ); + if( $addon_permissions ){ + self::_AdminPanelLinks($in_panel, $links, 'plugins', 'fa fa-plug', 'add'); + } + + //settings + $links = self::GetAdminGroup('settings'); + self::_AdminPanelLinks($in_panel, $links, 'Settings', 'fa fa-sliders', 'set'); + + //tools + $links = self::GetAdminGroup('tools'); + self::_AdminPanelLinks($in_panel, $links, 'Tools', 'fa fa-wrench', 'tool'); + + //notifications + if( \gp\admin\Tools::HasPermission('Admin/Notifications') ){ + $notifications = new \gp\admin\Notifications(); + $notifications->GetNotifications($in_panel); + } + + //username + ob_start(); + self::GetFrequentlyUsed($in_panel); + + echo '
  • '; + echo \gp\tool::Link('Admin/Preferences', $langmessage['Preferences']); + echo '
  • '; + + echo '
  • '; + echo \gp\tool::Link( + $page->title, + $langmessage['logout'], + 'cmd=logout', + ['data-cmd' => 'cnreq'] + ); + echo '
  • '; + + echo '
  • '; + echo \gp\tool::Link('Admin/About', 'About ' . CMS_NAME); + echo '
  • '; + + $links = ob_get_clean(); + self::_AdminPanelLinks($in_panel, $links, $gpAdmin['useralias'], 'fa fa-user-circle', 'use'); + + // stats + ob_start(); + echo '
  • ? Memory
  • '; + echo '
  • ? Max Memory
  • '; + echo '
  • ? Seconds
  • '; + echo '
  • ? Milliseconds
  • '; + echo '
  • 0 DB Queries
  • '; + $links = ob_get_clean(); + self::_AdminPanelLinks($in_panel, $links, 'Performance', 'fa fa-bar-chart', 'cms'); + + //resources + if( $page->pagetype === 'admin_display' ){ + ob_start(); + if( gp_remote_plugins && self::HasPermission('Admin_Addons') ){ + echo '
  • '; + echo \gp\tool::Link('Admin/Addons/Remote', $langmessage['Download Plugins']); + echo '
  • '; + } + if( gp_remote_themes && self::HasPermission('Admin_Theme_Content') ){ + echo '
  • '; + echo \gp\tool::Link('Admin_Theme_Content/Remote', $langmessage['Download Themes']); + echo '
  • '; + } + echo '
  • Support Forum
  • '; + echo '
  • Service Providers
  • '; + echo '
  • Official ' . CMS_NAME . ' Site
  • '; + echo '
  • Report A Bug
  • '; + + $links = ob_get_clean(); + self::_AdminPanelLinks($in_panel, $links, 'resources', 'fa fa-globe', 'res'); + + if( $in_panel ){ + echo '
    '; + echo CMS_NAME . ' ' . gpversion; + echo '
    '; + } + + } + } + + + /** + * Get the appropriate remote browse url if available + * + */ + public static function RemoteUrl($type){ + + if( $type == 'theme' || $type == 'themes' ){ + if( gp_remote_themes ){ + return addon_browse_path . '/Themes'; + } + } + + if( $type == 'plugin' || $type == 'plugins' ){ + if( gp_remote_plugins ){ + return addon_browse_path . '/Plugins'; + } + } + + return false; + } + + + /** + * Helper function for outputting link groups in AdminPanelLinks() + * as of 5.2-rc new params: + * @param string $class, CSS class(es) for panelgroup, TODO: better make it $attrs? + * @param string $badge, HTML for optional badge, TODO: should be better an array of separated class and content + * + */ + public static function _AdminPanelLinks($in_panel, $links, $lang_key, $icon_class, $panel_arg, $class='', $badge=''){ + global $langmessage; + + if( empty($links) ){ + return; + } + + $label = isset($langmessage[$lang_key]) ? $langmessage[$lang_key] : $lang_key; + + echo '
    '; + self::PanelHeading($in_panel, $label, $icon_class, $panel_arg, $badge); + echo ''; + echo '
    '; + echo '
    '; + } + + + public static function PanelHeading($in_panel, $label, $icon, $arg, $badge){ + global $gpAdmin; + + if( !$in_panel ){ + echo ''; + echo ' '; + echo '' . $label . ''; + echo ''; + echo '
    '; + return; + } + + echo ''; + echo ''; + echo '' . $label . ''; + echo $badge; + echo ''; + + if( $gpAdmin['gpui_vis'] == $arg ){ + echo '
    '; + }else{ + echo '
    '; + } + } + + + /** + * Get the links for the Frequently Used section of the admin toolbar + * + */ + public static function GetFrequentlyUsed($in_panel){ + global $langmessage, $gpAdmin; + + $expand_class = 'expand_child'; + if( !$in_panel ){ + $expand_class = 'expand_child_click'; + } + + //frequently used + echo '
  • '; + + echo ''; + echo $langmessage['frequently_used']; + echo ''; + if( $in_panel ){ + echo ''; + self::NewTitle($destination, 'Heading_Page', $content, $config, $new_index); + + // Child Page + $content = '

    A Child Page

    You can easily change the arrangement of all your pages using the ' . self::Install_Link_Content('Admin/Menu', 'Page Manager') . '.

    '; + self::NewTitle($destination, 'Child_Page', $content, $config, $new_index); + + // More + $content = '

    More

    +
    • ' . self::Install_Link_Content('About', 'About') . '
    • +
    • ' . self::Install_Link_Content('Contact', 'Contact') . '
    • +
    '; + self::NewTitle($destination, 'More', $content, $config, $new_index); + + // About + $content = '

    About ' . CMS_NAME . '

    ' . CMS_NAME . ' is a complete Content Management System (CMS) that can help you create rich and flexible web sites with a simple and easy to use interface.

    +

    ' . CMS_NAME . ' How To

    +

    Learn how to manage your files, + create galleries and more in the + ' . CMS_NAME . ' Documentation. +

    + +

    ' . CMS_NAME . ' Features

    +
      +
    • True WYSIWYG (Using CKEditor)
    • +
    • Galleries (Using ColorBox)
    • +
    • SEO Friendly Links
    • +
    • Free and Open Source (GPL)
    • +
    • Runs on PHP
    • +
    • File Upload Manager
    • +
    • Drag \'n Drop Theme Content
    • +
    • Deleted File Trash Can
    • +
    • Multiple User Administration
    • +
    • Flat File Storage
    • +
    • Fast Page Loading
    • +
    • Fast and Easy Installation
    • +
    • reCaptcha for Contact Form
    • +
    • HTML Tidy (when available)
    • +
    '; + self::NewTitle($destination, 'About', $content, $config, $new_index); + + //Copyright Notice + $file = $destination . '/data/_extra/Copyright_Notice/page.php'; + $content = '

    © $currentYear My ' . CMS_NAME . '

    '; + self::NewExtra($file, $content); + + //Header Contact + $file = $destination . '/data/_extra/Header_Contact/page.php'; + $content = ' $email +  +1 2345 6789 0'; + self::NewExtra($file, $content); + + //Header Social Media + $file = $destination . '/data/_extra/Header_SocialMedia/page.php'; + $content = ' + + + + + '; + self::NewExtra($file, $content); + + //Side_Menu + $file = $destination . '/data/_extra/Side_Menu/page.php'; + $content = '

    Join the ' . CMS_NAME . ' Community

    +

    Visit ' . CMS_READABLE_DOMAIN . ' to access the many available resources to help you get the most out of our CMS.

    + +

    (Edit this content by clicking "Edit", it's that easy!)

    '; + self::NewExtra($file, $content); + + //Header + $file = $destination . '/data/_extra/Header/page.php'; + $content = '

    ' . $config['title'] . '

    +

    ' . 'The Fast and Easy CMS' . '

    '; + self::NewExtra($file, $content); + + //Footer + $file = $destination . '/data/_extra/Footer/page.php'; + $content = '

    ' . CMS_NAME . ' Features

    +

    Easy to use True WYSIWYG Editing.

    +

    Flat-file data storage and advanced resource management for fast websites.

    +

    Community driven development

    +

    And More...

    +

    If you like ' . CMS_NAME . ', then you might also like + Less.php, + WhatCMS.org and + WhichCMS.org. +

    '; + self::NewExtra($file, $content); + + //Footer Column 1 + $file = $destination . '/data/_extra/Footer_Column_1/page.php'; + $content = '

    Footer Column 1

    '; + self::NewExtra($file, $content); + + //Footer Column 2 + $file = $destination . '/data/_extra/Footer_Column_2/page.php'; + $content = '

    Footer Column 2

    '; + self::NewExtra($file, $content); + + //Footer Column 3 + $file = $destination . '/data/_extra/Footer_Column_3/page.php'; + $content = '

    Footer Column 3

    '; + self::NewExtra($file, $content); + + //Footer Column 4 + $file = $destination . '/data/_extra/Footer_Column_4/page.php'; + $content = '

    Footer Column 4

    '; + self::NewExtra($file, $content); + + //Dropdown Divider + $file = $destination . '/data/_extra/Bootstrap_Dropdown_Divider/page.php'; + $content = ''; + self::NewExtra($file, $content); + + //Another example area + $file = $destination . '/data/_extra/Lorem/page.php'; + $content = '

    Heading

    +

    Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

    '; + self::NewExtra($file, $content); + + //Contact html + $file = $destination . '/data/_extra/Contact/page.php'; + $content = '

    Contact Us

    Use the form below to contact us, and be sure to enter a valid email address if you want to hear back from us.

    '; + self::NewExtra($file, $content); + + + //users + echo '
  • '; + $user_info = []; + $user_info['password'] = \gp\tool::hash($_POST['password'],'sha512'); + $user_info['passhash'] = 'sha512'; + $user_info['granted'] = 'all'; + $user_info['editing'] = 'all'; + $user_info['email'] = $_POST['email']; + + $users = []; + $username = $_POST['username']; + + //log user in here to finish user_info + if( $base_install ){ + gp_defined('gp_session_cookie', \gp\tool\Session::SessionCookie($config['gpuniq'])); + \gp\tool\Session::create($user_info, $username, $sessions); + } + $users[$username] = $user_info; + + if( !\gp\tool\Files::SaveData($destination . '/data/_site/users.php', 'users', $users) ){ + echo ''; + echo sprintf($langmessage['COULD_NOT_SAVE'], 'users.php'); + echo ''; + echo '
  • '; + return false; + } + echo ''; + echo sprintf($langmessage['_SAVED'], 'users.php'); + echo ''; + echo ''; + + //save config + //not using SaveConfig() because $config is not global here + echo '
  • '; + $config['file_count'] = self::$file_count; + if( !\gp\tool\Files::SaveData($destination . '/data/_site/config.php', 'config', $config) ){ + echo ''; + echo sprintf($langmessage['COULD_NOT_SAVE'], 'config.php'); + echo ''; + echo '
  • '; + return false; + } + echo ''; + echo sprintf($langmessage['_SAVED'], 'config.php'); + echo ''; + echo ''; + + + if( $base_install ){ + self::InstallHtaccess($destination); + } + + \gp\tool\Files::Unlock('write', gp_random); + + return true; + } + + + public static function NewTitle($dataDir, $title, $content, $config, $index){ + + $file = $dataDir . '/data/_pages/' . substr($config['gpuniq'], 0, 7) . '_' . $index[$title] . '/page.php'; + self::$file_count++; + + $file_sections = []; + $file_sections[0] = [ + 'type' => 'text', + 'content' => $content, + ]; + + $meta_data = [ + 'file_number' => self::$file_count, + 'file_type' => 'text', + ]; + + return \gp\tool\Files::SaveData($file, 'file_sections', $file_sections, $meta_data); + } + + public static function NewExtra($file, $content, $type="text"){ + $extra_content = [['type' => $type, 'content' => $content]]; + return \gp\tool\Files::SaveData($file, 'file_sections', $extra_content); + } + + + /** + * attempt to create an htaccess file + * .htaccess creation only works for base_installations because of the $dirPrefix variable + * This is for the rewrite_rule and TestResponse() which uses AbsoluteUrl() + * + * @access public + * @static + * @since 1.7 + * + * @param string $destination The root path of the installation + * @param array $config Current installation configuration + */ + public static function InstallHtaccess($destination){ + global $dirPrefix; + + //only proceed with save if we can test the results + if( \gp\tool\RemoteGet::Test() === false ){ + return; + } + + // only rewrite rules for IIS and Apache + if( !\gp\admin\Settings\Permalinks::IIS() && !\gp\admin\Settings\Permalinks::Apache() ){ + return; + } + + $GLOBALS['config']['homepath'] = false; //to prevent a warning from absoluteUrl() + $file = $destination . '/.htaccess'; + + $original_contents = false; + if( file_exists($file) ){ + $original_contents = file_get_contents($file); + } + + $contents = \gp\admin\Settings\Permalinks::Rewrite_Rules(true, $dirPrefix, $original_contents ); + + if( $contents === false ){ + return; + } + + $fp = @fopen($file, 'wb'); + if( $fp === false ){ + return; + } + + @fwrite($fp, $contents); + fclose($fp); + @chmod($file, 0666); + + //return .htaccess to original state + if( !\gp\admin\Settings\Permalinks::TestResponse() ){ + if( $original_contents === false ){ + unlink($file); + }else{ + $fp = @fopen($file, 'wb'); + if( $fp !== false ){ + @fwrite($fp, $original_contents); + fclose($fp); + } + } + } + + } + + + public static function Install_Link_Content($href, $label, $query='', $attr=''){ + + $query = str_replace('&', '&', $query); + $href = str_replace('&', '&', $href); + + if( !empty($query) ){ + $query = '?' . $query; + } + + return '' . $label . ''; + } + + + public static function AddCSs(){ + global $dataDir; + + echo ''; + } + +} diff --git a/include/install/install.php b/include/install/install.php new file mode 100644 index 0000000..4244d00 --- /dev/null +++ b/include/install/install.php @@ -0,0 +1,41 @@ + + + + + +Typesetter Installation + + + + + + + + +
    + +Run(); +echo \gp\tool::ErrorBuffer(false); +?> + +
    + diff --git a/include/install/update.css b/include/install/update.css new file mode 100644 index 0000000..737482d --- /dev/null +++ b/include/install/update.css @@ -0,0 +1,209 @@ + +body { + font-family: -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, + "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + background: #333; + font-size: 14px; +} + +a { + color: #4466aa; + text-decoration: none; + border-bottom: 1px dotted #869ece; +} + +h1, h2, h3, h4 { + font-weight: normal; + color: #333; +} + +h1 { + margin-top: 0; + padding-top: 0; + font-size: 40px; +} + +h2 { + font-size: 30px; +} + + +.wrapper { + position: relative; + width: 960px; + background: #fff; + margin: 1em auto; + border-radius: 5px; + padding: 40px; +} + +.fullwidth { + width: 100%; +} + +.styledtable { + border-collapse:collapse; + border-bottom: 1px solid #ccc; + border-spacing: 0; +} + +.styledtable td, +.styledtable th { + border-top: 1px solid #eee; + padding: 7px 20px; + text-align: left; + vertical-align: top; +} + +.styledtable th { + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + background-color: #eee; + font-weight: normal; + white-space: nowrap; +} + +.styledtable tr { + background: #fff; +} + +.styledtable tbody tr:nth-child(odd) { + background: #fbfbfb; +} + +.styledtable table td { + padding: 1px; + border: 0 none; +} + +.padded_table { + border-collapse: collapse; +} + +.padded_table > tbody > tr > td { + padding: 5px 8px; +} + +.lang_select { + position: absolute; + top: 40px; + right: 40px; +} + +.lang_select select, +.install_button { + font-size: 130%; + padding: 7px 9px; +} + +.lang_select option { +} + +.submit { + font-size: 130%; + padding: 7px 9px; + margin: 7px 9px 7px 0; +} + + +.sm { + font-size: smaller; +} + +input.text { + font-size: 12px; + padding: 4px 6px; + width: 20em; + border: 1px solid #aaa; + margin: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; +} + +input.text:focus { + border-color: #333; +} + +.failed { + color: #FF0000; +} + +.passed { + color: #009900; +} +.passed_orange { + color: orange; +} + +.code { + margin: 4px 0; + padding: 5px 7px; + white-space: nowrap; + background-color: #f5f5f5; + display: block; + } + +.inline_message { + margin: 1em 0; + font-size: 19px; + padding: 7px 0; + + border-top: 1px dashed #aaa; + border-bottom: 1px dashed #aaa; +} +.inline_message .green { + color: #009900; + font-weight: normal; +} + + +.steps li { + font-weight: bold; + font-size: 13px; + color: #444; +} +.steps .current { + color: #009900; +} +.steps .done { + color: #aaa; +} +.progress li { + padding: 5px 20px 5px 0; +} + +.formtable td, +.formtable th { + padding: 5px 20px 5px 0; + text-align: left; + vertical-align: top; +} + +.nowrap { + white-space: nowrap; +} + +ul.install_status { + list-style: none; + margin: 0; + padding: 0; +} +ul.install_status li { + margin: 4px 0; + padding: 4px 7px; +} + +ul.install_status .failed { + border: 1px solid #e6da93; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; + background: #fff2a3; + color: #4d4931; +} diff --git a/include/install/update.php b/include/install/update.php new file mode 100644 index 0000000..cf65f71 --- /dev/null +++ b/include/install/update.php @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + +
    + +

    Typesetter Updater

    + +GetContent(); ?> + +
    + + + diff --git a/include/js/admin.js b/include/js/admin.js new file mode 100644 index 0000000..732f744 --- /dev/null +++ b/include/js/admin.js @@ -0,0 +1,1811 @@ +/*global $gp:false, gpui:false, gplinks:false, gpinputs:false isadmin:false, gpBase:false, strip_from:false, gpRem:false, gpBLink:false */ +/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, jquery:true, indent:4, maxerr:100, newcap:false, white:false*/ +//"use strict"; + +var gp_editor = false; + +$gp.curr_edit_id = null; +$gp.interface = []; // storage for editing interfaces +$gp.cached = {}; +$gp.defaults = {}; +$gp.editors = []; // storage for editing objects + + +/** + * Get the coordinates for positioning editable area overlays + * + */ +$gp.Coords = function(area){ + if( area.hasClass('inner_size') ){ + area = area.children().first(); + } + var loc = area.offset(); + loc.w = area.outerWidth(); + loc.h = area.outerHeight(); + return loc; +}; + + +/** + * Create a
    in the #gp_admin_html space + * + */ +$gp.div = function(id,div_class){ + var div = $('#'+id); + div_class = div_class || ''; + if( div.length === 0 ){ + div = $('
    ').appendTo('#gp_admin_html'); + } + return div; +}; + + +/** + * Dynamically load inline editing + * + */ +$gp.links.inline_edit_generic = function(evt,arg){ + + evt.preventDefault(); + + var area_id = $gp.AreaId( $(this) ); + + $gp.LoadEditor(this.href, area_id, arg); + + + //show editor when a edit link is clicked in the + var $target = $(evt.target); + if( $target.closest('.panel_tabs').length && typeof(gp_editing) != 'undefined' ){ + if( $target.data('mode') == 'extra' ){ + gp_editing.is_extra_mode = true; + }else{ + gp_editing.is_extra_mode = false; + } + gp_editing.ShowEditor(); + } +} + +$gp._loadingEditor = false; +$gp.LoadEditor = function(href, area_id, arg){ + + area_id = area_id || 0; + + if( area_id === $gp.curr_edit_id ){ + return; + } + + if( $gp._loadingEditor ){ + console.log('editor still loading'); + return; + } + + $gp._loadingEditor = true; + + //first time editing, get default $gp.links, $gp.inputs, $gp.response + if( typeof(gp_editing) == 'undefined' ){ + $gp.defaults['links'] = $gp.Properties($gp.links); + $gp.defaults['inputs'] = $gp.Properties($gp.inputs); + $gp.defaults['response'] = $gp.Properties($gp.response); + } + + + $gp.CacheInterface(function(){ + + //set the current editing interface aside so the new one can be created + if( typeof(gp_editing) !== 'undefined' ){ + if( gp_editing.RestoreCached(area_id) ){ + $gp._loadingEditor = false; + return; + } + }else{ + $gp.LoadStyle('/include/css/inline_edit.css'); + $gp.LoadStyle('/include/css/manage_sections.css'); + // $gp.LoadStyle('/include/css/manage_sections_compact.css'); // alternative compact style + } + + $gp.curr_edit_id = area_id; + var $edit_div = $gp.CurrentDiv(); + + $gp.DefinedObjects(); + $gp.loading(); + + + //legacy inline editing support + //can also be used for development/troubleshooting + if( typeof(gplinks[arg]) === 'function' ){ + gplinks[arg].call(this,arg,evt); + $gp._loadingEditor = false; + return; + } + + var script = strip_from(href,'#'); + script += '&gpreq=json&defined_objects='+$gp.DefinedObjects(); + + if( arg != 'manage_sections' ){ + script += '&cmd=inlineedit&area_id='+area_id+'§ion='+$edit_div.data('gp-section'); + } + + + //get the new editor + $.getScript( script,function(data){ + if( data === 'false' ){ + alert($gp.error); + $gp.loaded(); + $gp._loadingEditor = false; + return; + } + + if( typeof(gp_editor.wake) == 'function' ){ + gp_editor.wake(); + } + $gp._loadingEditor = false; + }); + + }); +}; + +/** + * Get object properties + * + */ +$gp.Properties = function(obj){ + var properties = []; + for( var i in obj ){ + if( obj.hasOwnProperty(i) ){ + properties.push(i); + } + } + return properties; +} + + +/** + * Get the current editing div + * + */ +$gp.CurrentDiv = function(){ + return $('#ExtraEditArea'+$gp.curr_edit_id); +} + + +/** + * Cache the current editing interface + * + */ +$gp.CacheInterface = function(callback){ + + + $gp.CurrentDiv().removeClass('gp_edit_current'); + + + //if gp_editing is not defined then we don't have anything to cache yet + if( typeof(gp_editing) == 'undefined' ){ + callback.call(); + return; + } + + + //only continue if we can save + gp_editing.SaveChanges(function(){ + + + if( typeof(gp_editor.sleep) == 'function' ){ + gp_editor.sleep(); + } + + + $gp.interface[$gp.curr_edit_id] = $('#ck_area_wrap').children().detach(); + $gp.editors[$gp.curr_edit_id] = gp_editor; + + + //cache $gp.links that were defined by the current gp_editor + $gp.cached[$gp.curr_edit_id] = {}; + $gp.CacheObjects( 'links' ); + $gp.CacheObjects( 'inputs' ); + $gp.CacheObjects( 'response' ); + + + $('.cktabs .ckeditor_control.selected').removeClass('selected'); + + callback.call(); + }); +} + + +/** + * Cache $gp.links, $gp.inputs and $gp.Response + * ?? gpresponse, gplinks, gpinputs ?? + */ +$gp.CacheObjects = function(type, cache_loc){ + + var from = $gp[type]; + $gp.cached[$gp.curr_edit_id][type] = {}; + + for( var i in from ){ + if( !from.hasOwnProperty(i) ){ + continue; + } + + if( $gp.defaults[type].indexOf(i) > -1 ){ + continue; + } + $gp.cached[$gp.curr_edit_id][type][i] = from[i]; + } +} + + +/** + * Restore $gp.links, $gp.inputs and $gp.Response + * ?? gpresponse, gplinks, gpinputs ?? + */ +$gp.RestoreObjects = function(type, id){ + + var from = $gp.cached[id][type]; + + for( var i in from ){ + if( !from.hasOwnProperty(i) ){ + continue; + } + $gp[type][i] = from[i]; + } +} + + +/** + * Send to the server which javascript objects are already defined + * + */ +$gp.defined_objects = []; +$gp.DefinedObjects = function(){ + + //get all objects the first time + if( typeof(gp_editing) == 'undefined' ){ + for( var i in window ) { + if( typeof(window[i]) != 'object' ){ + continue; + } + $gp.defined_objects.push(i); + } + } + + //compare with $gp.defined_objects + var objects = []; + for( var i in window ) { + if( typeof(window[i]) != 'object' ){ + continue; + } + if( $gp.defined_objects.indexOf(i) != -1 ){ + continue; + } + objects.push(i); + } + + return objects.join(','); +} + + +/** + * Remote Browse + * @param object evt Event object + * + */ +$gp.links.remote = function(evt){ + evt.preventDefault(); + var src = $gp.jPrep(this.href,'gpreq=body'); + + //can remote install + if( gpRem ){ + var pathArray = window.location.href.split( '/' ); + var url = pathArray[0] + '//' + pathArray[2]+gpBase; + if( window.location.href.indexOf('index.php') > 0 ){ + url += '/index.php'; + } + + src += '&inUrl='+encodeURIComponent(url) + + '&gpRem='+encodeURIComponent(gpRem); + } + + //40px margin + 17px*2 border + 20px padding + 10 (extra padding) = approx 130 + // var height = $gp.$win.height() - 130; + + var opts = {context:'iframe',width:780}; + + var iframe = '').appendTo('body'); + + $form.attr('target', iframe_id).one('submit',function(){ + $iframe.one('load',function (){ + var contents = $iframe.contents().find('html').html(); + options.finish(contents, filename, settings); + + setTimeout(function (){ + $iframe.remove(); + },10); + }); + }).trigger('submit'); + }); + } + + return $(this).each(function(){ + $this = $(this); + + if( supports_multiple_files ){ + $this.attr('multiple','multiple'); + $this.on('change.auto_upload', upload); + }else{ + init_legacy.call(this); + } + + + $this.on('destroy.auto_upload', function() { + $this.off('.auto_upload'); + }); + + }); + }; + + $.fn.auto_upload.defaults = { + start: function(name, settings) { + return true; + }, + progress: function(progress, name, settings) {}, + finish: function(response, name, settings) {}, + error: function(name, error, settings) {} + }; + + +})(jQuery); diff --git a/include/js/layout_editor.js b/include/js/layout_editor.js new file mode 100644 index 0000000..d3c7507 --- /dev/null +++ b/include/js/layout_editor.js @@ -0,0 +1,454 @@ +$gp.EditLayout = { + + css_editor : {}, + + customizer : {}, + + isDirty : false, + + checkDirty : function(){ + var checks = + $gp.EditLayout.css_editor.checkDirty() || + $gp.EditLayout.customizer.checkDirty(); + + $gp.EditLayout.isDirty = checks; + return checks; + }, + + resetDirty : function(){ + $gp.EditLayout.css_editor.resetDirty(); + $gp.EditLayout.customizer.resetDirty(); + }, + + bindEvents : function(){ + + $('.layout_editor_tabs .tab_switch') + .each($gp.EditLayout.switchTab) + .on('click', $gp.EditLayout.switchTab); + + // preview button + $gp.inputs.preview_changes = function(evt){ + $gp.loading(); + }; + + // save button TODO! + $gp.inputs.save_changes = function(evt){ + var _this = $gp.EditLayout.css_editor; + + //_this.textarea.removeClass('edited'); + //_this.data = _this.textarea.val(); + + setTimeout(function(){ + + $gp.EditLayout.resetDirty(); + + $('button[data-cmd="preview_changes"], ' + + 'button[data-cmd="save_changes"], ' + + 'button[data-cmd="reset_changes"]') + .addClass('gpdisabled') + .prop('disabled', true); + }, 150); + + $gp.loading(); + }; + + // reset button + $gp.inputs.reset_changes = function(evt){ + // reset css editor + $gp.EditLayout.css_editor.resetValues(); + // reset customizer + $gp.EditLayout.customizer.resetValues(); + // update buttons + $gp.EditLayout.updateButtons(); + + if( $gp.EditLayout.isDirty ){ + $gp.inputs.save_changes(); + } + } + + $(window).on('beforeunload', function(evt) { + $gp.EditLayout.checkDirty(); + if( $gp.EditLayout.isDirty ){ + return 'Warning: There are unsaved changes. Proceed anyway?'; + } + }); + }, + + switchTab : function(evt){ + var activate = evt.type == 'click' || $(this).hasClass('active'); + if( activate ){ + $(this) + .addClass('active') + .siblings().removeClass('active'); + $('.' + $(this).data('rel')) + .addClass('active') + .siblings().removeClass('active'); + } + $gp.EditLayout.css_editor.setSize(); + }, + + updateButtons : function(){ + $gp.EditLayout.checkDirty(); + + // always keep the preview button enabled + $('button[data-cmd="preview_changes"]') + .toggleClass('gpdisabled', false) + .prop('disabled', false); + + $('button[data-cmd="save_changes"], ' + + 'button[data-cmd="reset_changes"]') + .toggleClass('gpdisabled', !$gp.EditLayout.isDirty) + .prop('disabled', !$gp.EditLayout.isDirty); + }, + + init : function(){ + $gp.EditLayout.css_editor.init(); + $gp.EditLayout.customizer.init(); + $gp.EditLayout.bindEvents(); + } + +}; + + +// customizer +$gp.EditLayout.customizer = { + + cache : null, + cache_arr : null, + + data : null, + data_arr : null, + + isDirty : false, + + checkDirty : function(){ + var _this = $gp.EditLayout.customizer; + + _this.data = _this.getData(); + var is_dirty = _this.data !== _this.cache; + _this.isDirty = is_dirty; + _this.indicateDirty(); + + return is_dirty; + }, + + resetValues : function(){ + var _this = $gp.EditLayout.customizer; + + // console.log('customizer resetChanges'); // TODO remode + // console.log('_this.cache_arr = ', _this.cache_arr); // TODO remode + + $.each(_this.cache_arr, function(i, cache_obj){ + var input_name = cache_obj.name; + var cache_value = cache_obj.value; + var $input = $('.customizer_area [name="' + input_name + '"]'); + if( $input.val() !== cache_value ){ + $input.val(cache_value).trigger('input'); + } + if( $input.is('.customizer_checkbox_alias') ){ + $input.next('[type="checkbox"]') + .prop('checked', cache_value == 'on'); + } + }); + + _this.resetDirty(); + }, + + resetDirty : function(){ + var _this = $gp.EditLayout.customizer; + + _this.data = _this.getData(); + _this.cache = _this.data; + _this.dataArray = _this.getDataArray(); + _this.cacheArray = _this.dataArray; + _this.isDirty = false; + _this.indicateDirty(); + }, + + indicateDirty : function(what){ + var _this = $gp.EditLayout.customizer; + var what = typeof(what) != 'undefined' ? what : _this.isDirty; + $('.tab_switch[data-rel="customizer_area"]') + .toggleClass('is_dirty', what); + $('input#gp_layout_save_customizer').val((what ? 'on' : '')); + }, + + getData : function(){ + var data = $('.customizer_area [name^="customizer["]').serialize(); + // console.log('customizer data = ', data); + return data; + }, + + getDataArray : function(){ + var data_array = $('.customizer_area *[name^="customizer["]').serializeArray(); + // console.log('customizer data array = ', data_array); + return data_array; + }, + + bindEvents : function(){ + + // expand/collapse customizer sections + $gp.links.toggle_customizer_section = function(evt){ + $this = $(this); + $this.siblings('.customizer_controls') + .slideToggle( + 300, + function(){ + $(this).closest('.customizer_section') + .toggleClass('collapsed'); + } + ); + // collapse others + $this.closest('.customizer_area') + .find('.customizer_section:not(.collapsed) .customizer_controls') + .slideUp( + 300, + function(){ + $(this).closest('.customizer_section') + .addClass('collapsed'); + } + ); + }; + + + // detect changes + $('.customizer_area [name^="customizer["]').on('input change', function(){ + // console.log('change evt fired'); + $gp.EditLayout.customizer.checkDirty(); + $gp.EditLayout.updateButtons(); + }) + + // select file + $gp.links.customizer_select_file = function(evt){ + var $control_group = $(this).closest('.customizer_file_group'); + var $input = $control_group.find('.customizer_file_url'); + + gp_editor = { + FinderSelect : function(file_url){ // called by finder on file select + if( file_url != '' ){ + $input.val(file_url).trigger('change'); + } + return true; + } + }; + + var finderPopUp = window.open( + gpFinderUrl, + 'gpFinder', + 'menubar=no,width=960,height=640' + ); + + if( window.focus ){ + finderPopUp.focus(); + } + }; + + // toggle checkbox - set hidden sibling input value to on|off + $gp.inputs.toggle_customizer_checkbox = function(evt){ + $this = $(this); + $(this).prev('input[type="hidden"]') + .val($this.prop('checked') ? 'on' : 'off') + .trigger('change'); + }; + + // change file input value + $('.customizer_file_url').on('change', function(){ + var value = $(this).val(); + var is_image = value.match(/\.(jpg|jpeg|png|apng|gif|webp|avif|svg|bmp|ico)$/i) !== null; + // console.log('is_image = ', is_image); + $(this).closest('.customizer_file_group') + .find('.customizer_image_preview') + .toggle(is_image) + .find('img') + .attr('src', (is_image ? value : '')); + }); + + // init colorpicker fields + $('.customizer_colorpicker_group input').each(function(){ + $(this).css({ + 'color' : $gp.getContrastColor($(this).val()) + }); + }); + + $('.customizer_colorpicker_group').colorpicker({ + container : true, + component : 'input' + }).on('changeColor', function(){ + var color_val = $(this).find('input').val(); + $(this).find('input').css({ + 'background-color' : color_val, + 'color' : $gp.getContrastColor(color_val) + }); + }).on('hidePicker', function(){ + $(this).find('input').trigger('change'); + }); + }, + + init : function(){ + var _this = $gp.EditLayout.customizer; + _this.cache = _this.getData(); + _this.data = _this.cache; + _this.cache_arr = _this.getDataArray(); + _this.data_arr = _this.cache_arr; + _this.bindEvents(); + }, +}; + + +// CSS/LESS/SCSS editor +$gp.EditLayout.css_editor = { + + editor : null, + + config : { + mode : 'text/x-less', + lineWrapping : false, + lineNumbers : true + }, + + textarea : null, + + cache : null, + + data : null, + + isDirty : false, + + checkDirty : function(){ + var _this = $gp.EditLayout.css_editor; + + var is_dirty = _this.data !== _this.cache; + _this.isDirty = is_dirty; + _this.indicateDirty(); + return is_dirty; + }, + + resetValues : function(){ + var _this = $gp.EditLayout.css_editor; + + _this.editor.setValue(_this.cache); + _this.editor.clearHistory(); + _this.data = _this.cache; + + _this.resetDirty(); + }, + + resetDirty : function(){ + var _this = $gp.EditLayout.css_editor; + + _this.data = _this.getData(); + _this.cache = _this.data; + _this.isDirty = false; + _this.indicateDirty(); + }, + + indicateDirty : function(what){ // what = optional boolen + var _this = $gp.EditLayout.css_editor; + var what = typeof(what) != 'undefined' ? what : _this.isDirty; + $('.tab_switch[data-rel="css_editor_area"]') + .toggleClass('is_dirty', what); + $('input#gp_layout_save_css').val((what ? 'on' : '')); + }, + + getData : function(){ + var _this = $gp.EditLayout.css_editor; + + var data = _this.editor.getValue(); + // _this.data = data; + return data; + }, + + getMode : function(){ + var _this = $gp.EditLayout.css_editor; + + var mode = _this.textarea.data('mode'); + switch(mode){ + case 'scss': + return 'text/x-scss'; + + case 'less': + default: + return 'text/x-less'; + } + }, + + setSize : function(){ + var _this = $gp.EditLayout.css_editor; + + var parent = _this.textarea.parent(); + //shrink the editor so we can get the container size + _this.editor.setSize(225, 100); + _this.editor.setSize(225, parent.height() - 5); + }, + + bindEvents : function(){ + var _this = $gp.EditLayout.css_editor; + + // events may change in future codemirror versions(!) + _this.editor.on('change', function(evt){ + _this.data = _this.getData(); + _this.checkDirty(); + // _this.textarea.toggleClass('edited', _this.isDirty); + $gp.EditLayout.updateButtons(); // TODO + }); + + $(window).on('resize', function(){ + _this.setSize(); + }).trigger('resize'); + }, + + init : function(){ + var _this = $gp.EditLayout.css_editor; + + _this.textarea = $('#gp_layout_css'); + if( !_this.textarea.length ){ + return false; + } + _this.config.mode = _this.getMode(); + _this.editor = CodeMirror.fromTextArea( + _this.textarea.get(0), + _this.config + ); + _this.data = _this.getData(); + _this.cache = _this.data; + + _this.bindEvents(); + } +}; + + +$gp.getContrastColor = function(color_val){ + if( color_val.trim() == '' ){ + return '#000'; + } + var $temp_elem = $('') + .appendTo('body') + .css('color', color_val); + var color = getComputedStyle($temp_elem.get(0)).color; + $temp_elem.remove(); + + if( color == 'transparent'){ + return '#000'; + } + if( color.indexOf('rgb') !== -1 ){ + var color_array = color + .substring(color.indexOf('(') + 1, color.length - 1) + .replace(/ /g, '') + .split(','); + // console.log('color_array = ', color_array); + if( color_array.length == 4 && color_array[3] <= 0.5 ){ + return '#000'; + } + var r = parseInt(color_array[0]); + var g = parseInt(color_array[1]); + var b = parseInt(color_array[2]); + var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000; + return yiq >= 128 ? '#000' : '#fff'; + } + return 'red'; // color parsing failed +}; + + +$(function(){ + $gp.EditLayout.init(); +}); diff --git a/include/js/login.js b/include/js/login.js new file mode 100644 index 0000000..ce66de5 --- /dev/null +++ b/include/js/login.js @@ -0,0 +1,47 @@ + + +$(function(){ + // if( typeof(IE_LT_10) != 'undefined' && IE_LT_10 ){ + if( window.navigator.userAgent.match(/(MSIE|Trident)/) ){ + $('#browser_warning').show(); + } + $('#loginform .login_text').first().find('input').trigger('focus'); + + + window.setTimeout(function(){ + $('#login_timeout').show(); + $('#login_form').hide(); + },500000);//10 minutes would be 600,000 + + + //don't send plaintext password if possible + //send instead md5 and sha1 encrypted strings + $('#login_form').on('submit', function(){ + if( this.encrypted.checked ){ + var pwd = this.password.value; + var nonce = this.login_nonce.value; + this.pass_md5.value = hex_sha1(nonce+hex_md5(pwd)); + this.pass_sha.value = hex_sha1(nonce+hex_sha1(pwd)); + this.pass_sha512.value = sha512(pwd); + this.password.value = ''; + + this.user_sha.value = hex_sha1(nonce+this.username.value); + this.username.value = ''; + } + }); + + function sha512(pwd){ + + for(var i = 0; i < 50; i++ ){ + var ints = pwd.replace(/[a-f]/g,''); + var salt_start = parseInt(ints.substr(0,1)); + var salt_len = parseInt(ints.substr(2,1)); + var salt = pwd.substr(salt_start,salt_len); + var shaObj = new jsSHA(pwd+salt,'TEXT'); + pwd = shaObj.getHash('SHA-512', 'HEX'); + } + + return pwd; + } + +}); diff --git a/include/js/main.js b/include/js/main.js new file mode 100644 index 0000000..4635110 --- /dev/null +++ b/include/js/main.js @@ -0,0 +1,746 @@ +//"use strict"; + +var gplinks={}, gpinputs={}, gpresponse={}; + +/** + * $gp object + * + * + */ +var $gp = { + + inputs : {}, + response : {}, + error : 'There was an error processing the last request. Please reload this page to continue.', + cookie_cmd : false, + + /** + * Handler for loading json content + * + */ + jGoTo : function(a, this_context){ + $gp.loading(); + a = $gp.jPrep(a); + $.getJSON(a, function(data, textStatus, jqXHR){ + $gp.Response.call(this_context, data, textStatus, jqXHR); + }); + }, + + + /** + * Reload page with arguments (a) set as a cookie + * if samepage is false, then it will take user to a.href + * + */ + cGoTo : function(a,samepage){ + + var $link = $(a); + var query = a.search; + var nonce = $link.data('nonce'); + if( nonce ){ + query += '&verified=' + encodeURIComponent(nonce); + } + + $gp.SetCookieCmd(query); + + if( samepage ){ + $gp.Reload(); + }else{ + window.location = strip_from(strip_from(a.href, '#'), '?'); + } + + }, + + + /** + * Post request to server + * + */ + post : function(this_context, data){ + $gp.loading(); + var frm = $(this_context).closest('form'); + + //needed when $gp.post is called without an input click + var b = frm.serialize() + + '&verified=' + encodeURIComponent(post_nonce); + + if( this_context.nodeName === 'INPUT' || this_context.nodeName === 'BUTTON' ){ + b += '&' + encodeURIComponent(this_context.name) + + '=' + encodeURIComponent(this_context.value); + } + if( data ){ + b += '&' + data; + } + + $.post( + $gp.jPrep(frm.attr('action')), + b, + function(data, textStatus, jqXHR){ + $gp.Response.call(this_context, data,textStatus, jqXHR); + }, + 'json' + ); + return false; + }, + + + /** + * POST a link to the server + * + */ + post_link : function(lnk){ + $gp.loading(); + var $lnk = $(lnk); + var data = strip_to(lnk.search, '?') + + '&gpreq=json&jsoncallback=?' + + '&verified=' + encodeURIComponent($lnk.data('nonce')); + $.post( + strip_from(lnk.href, '?'), + data, + function(data, textStatus, jqXHR){ + $gp.Response.call(lnk, data, textStatus, jqXHR); + }, + 'json' + ); + }, + + /** + * Post content with Typesetter's verified value + * Arguments order is same as jQuery's $.post() + * + */ + postC : function(url, data, callback, datatype, this_context){ + callback = callback || $gp.Response; + datatype = datatype || 'json'; + + if( typeof(data) === 'object' ){ + data = $.param(data); + } + + data += '&verified=' + encodeURIComponent(post_nonce); + if( datatype === 'json' ){ + data += '&gpreq=json&jsoncallback=?'; + } + + $.post( + strip_from(url, '?'), + data, + function(data, textStatus, jqXHR){ + callback.call(this_context, data, textStatus, jqXHR); + }, + datatype + ); + }, + + + /** + * Return the current colorbox settings + * + */ + cboxSettings : function(options){ + options = options || {}; + + if( typeof(colorbox_lang) != 'object' ){ + colorbox_lang = {}; + } + return $.extend( + colorbox_lang, + { + opacity : 0.75, + maxWidth : '90%', + maxHeight : '90%' + }, + options + ); + }, + + /** + * Simple method for creating/erasing cookies + * + */ + Cookie : function(name, value, days){ + var expires = ''; + if( days ){ + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + document.cookie = name + '=' + value+expires + '; path=/'; + }, + + + /** + * Remove cookie command + * + */ + SetCookieCmd : function(query){ + $gp.Cookie('cookie_cmd', encodeURIComponent(query), 1); + $gp.cookie_cmd = true; + }, + + + /** + * Prepare a query for an ajax request + * + */ + jPrep : function(query, args){ + args = typeof(args) === 'undefined' ? 'gpreq=json&jsoncallback=?' : args; + query = strip_from(query, '#'); + + if( query.indexOf('?') === -1 ){ + query += '?'; + }else if( query.indexOf('?') !== (query.length -1) ){ + query += '&'; + } + + return query + args; + }, + + + /** + * Handle ajax responses + * + */ + Response : function(data, textStatus, jqXHR){ + + try{ + if( typeof(gp_editing) == 'undefined' ){ + $gp.CloseAdminBox(); + } + }catch(a){} + + try{ + $.fn.colorbox.close(); + } catch(a){} + + var this_context = this; + + $.each(data,function(i, obj){ + + if( typeof($gp.response[obj.DO]) === 'function' ){ + $gp.response[obj.DO].call(this_context, obj, textStatus, jqXHR); + return; + } + + if( typeof(gpresponse[obj.DO]) === 'function' ){ + console.log('gpresponse is deprecated as of 3.6'); + gpresponse[obj.DO].call(this_context, obj, textStatus, jqXHR); + return; + } + + switch(obj.DO){ + case 'replace': + CallFunc(obj.SELECTOR, 'replaceWith', obj.CONTENT); + break; + + case 'inner': + CallFunc(obj.SELECTOR, 'html', obj.CONTENT); + break; + + case 'gpabox': + case 'admin_box_data': // @deprecated 5.2 + var opts = {}; + if( $(this_context).closest('#gp_admin_box') ){ + // replace the content of the currently open + // admin box if the link the user clicked on was in the admin box + opts.replaceBox = true; + } + $gp.AdminBoxC(obj.CONTENT, opts); + break; + + case 'messages': + $('.messages').detach(); + $(obj.CONTENT).appendTo('body').show().css({ 'top' : 0 }); + break; + + case 'reload': + $gp.Reload(); + break; + + //standard functions + default: + CallFunc(obj.SELECTOR, obj.DO, obj.CONTENT); + break; + } + }); + + function CallFunc(sel, func, arg){ + + if( sel == 'window' ){ + sel = window; + } + + var $selected = $(sel); + if( typeof($selected[func]) == 'function' ){ + $selected[func](arg); + }else{ + console.log('func not found for sel=', sel, ' ,func=', func); + } + } + + $gp.loaded(); + }, + + + /** + * Display an overlay to indicate loading process + * + */ + loading : function(){ + + var $loading = $('#loading1'); + if( $loading.length == 0 ){ + $loading = $('
    ') + .append('') + .appendTo('body'); + } + + $loading + .css({'z-index' : 99000}) + .fadeIn(); + }, + + + /** + * Hide loading overlay + * + */ + loaded :function(){ + $('#loading1').clearQueue().fadeOut(); + }, + + + /** + * Assign values to the form based on hidden input elements + * + */ + CopyVals : function(selector, lnk){ + + var c = $(selector).find('form').get(0); + if( c ){ + $(lnk).find('input').each(function(i, j){ + if( c[j.name] ){ + c[j.name].value = j.value; + } + }); + } + }, + + + /** + * Reload the current page + * Use window.location.reload(true) to prevent the browser + * from using the cached page unless it was a post request + * + */ + Reload : function(){ + if( typeof(req_type) && req_type == 'post' ){ + window.location.href = strip_from(window.location.href, '#'); + }else{ + window.location.reload(true); + } + }, + + + /** + * Link handlers + * + */ + links : { + + /** + * Use colorbox + * + */ + gallery : function(evt, selector){ + + evt.preventDefault(); + + var rel = $(this).attr('rel'); + + if( selector === '' ){ + selector = this; + }else if( rel ){ + selector = 'a[rel="' + rel + '"]'; + }else{ + selector = 'a.' + selector; + } + + var settings = { + resize : true, + title : function(){ + var a = $(this); + var caption = + a.closest('li').find('.caption').data("originalContent") || + a.closest('li').find('.caption').text() || + a.attr('title') || // backwards compat + ''; + return caption; + } + }; + + if( rel ){ + settings.rel = rel; + } + + $.colorbox.remove(); + $(selector).colorbox( + $gp.cboxSettings(settings) + ); + + $(this).trigger('click.cbox'); + } + + } + +} + + +//erase cookie_cmd as soon as possible +$gp.Cookie('cookie_cmd', '', -1); + + +/** + * Onload + * + */ +$(function(){ + + var $document = $(document); + + //add a class to the body + //this also affects the display of elements using the req_script css class + $('body').addClass('STCLASS'); + + + /** + * Remove cookie_cmd before a new page is loaded + * Prevents a cookie_cmd from another browser tab being sent along with a request in the current tab + * + */ + $(window).on('beforeunload', function(evt){ + if( !$gp.cookie_cmd ){ + $gp.Cookie('cookie_cmd', '', -1); + } + }); + + + /** + * Handle AJAX errors + * + */ + $document.ajaxError(function(event, XMLHttpRequest, ajaxOptions, thrownError){ + $gp.loaded(); + + // + if( XMLHttpRequest.statusText == 'abort' ){ + return; + } + + // don't use this error handler if another one is set for the ajax request + if( typeof(ajaxOptions.error) === 'function' ){ + return; + } + + if( thrownError == '' ){ + return; + } + + // collect some debug info + var debug_info = { + thrownError : thrownError + }; + + // add error details + var detail_keys = ['name', 'message', 'fileName', 'lineNumber', 'columnNumber', 'stack']; + for(var i = 0; i < detail_keys.length; i++){ + if( thrownError.hasOwnProperty(detail_keys[i]) ){ + debug_info[detail_keys[i]] = thrownError[detail_keys[i]]; + } + } + + //get the location of the error + if( thrownError.hasOwnProperty('lineNumber') ){ + var num = thrownError.lineNumber; + var lines = XMLHttpRequest.responseText.split('\n'); + + debug_info['Line-' + (num-1)] = lines[num - 2]; + debug_info['Line-' + num] = lines[num - 1]; + debug_info['Line-' + (num+1)] = lines[num]; + } + + debug_info.responseStatus = XMLHttpRequest.status; + debug_info.statusText = XMLHttpRequest.statusText; + debug_info.url = ajaxOptions.url; + debug_info.type = ajaxOptions.type; + debug_info.browser = navigator.userAgent; + debug_info.responseText = XMLHttpRequest.responseText; + + if( ajaxOptions.data ){ + debug_info.ajaxdata = ajaxOptions.data.substr(0, 100); + } + + // log everything if possible + if( window.console && console.log ){ + console.log(debug_info); + } + + // send to Typesetter bug tracker + if( typeof(debugjs) !== 'undefined' && debugjs === 'send' ){ + + if( ajaxOptions.data ){ + debug_info.data = ajaxOptions.data; + } + + debug_info.cmd = 'javascript_error'; + $.ajax({ + type : 'POST', + url : 'https://www.typesettercms.com/Resources', + data : debug_info, + success : function(){}, + error : function(){} + }); + } + + //display message to user + if( typeof($gp.AdminBoxC) !== 'undefined' && typeof(JSON) != 'undefined' ){ + delete debug_info.responseText; //otherwise it's too long + + var _debug = JSON.stringify(debug_info); + _debug = b64Encode(_debug); + _debug = _debug.replace(/\=/g, ''); + _debug = _debug.replace(/\+/g, '-').replace(/\//g, '_'); + var url = 'https://www.typesettercms.com/index.php/Debug?data=' + _debug; + $gp.AdminBoxC( + '

    Error

    ' + $gp.error + '

    ' + + 'More Info
    ' + ); + }else{ + alert($gp.error); + } + + }); + + + /** + * Unicode safe base64 encode + * + */ + function b64Encode(str){ + return btoa( + encodeURIComponent(str).replace( + /%([0-9A-F]{2})/g, + function(match, p1){ + return String.fromCharCode('0x' + p1); + } + ) + ); + } + + + /** + * Handle clicks on forms + * + */ + $document.on('click', 'input,button', function(evt){ + + var $this = $(this); + + //add a unique verifiable string to confirm posts are + $(this.form) + .filter('[method=post]') + .filter(':not(:has(input[type=hidden][name=verified]))') + .append(''); + + + //html5 validation + if( $this.hasClass('gpvalidate') && + typeof(this.form.checkValidity) == 'function' && + !this.form.checkValidity() + ){ + return; + } + + //confirm prompt + if( $this.hasClass('gpconfirm') && !confirm(this.title) ){ + evt.preventDefault(); + return; + } + + //get the first class + var cmd = $this.data('cmd'); + if( !cmd ){ + cmd = strip_from($this.attr('class'), ' '); //deprecated + } + + if( typeof($gp.inputs[cmd]) === 'function' ){ + return $gp.inputs[cmd].call(this, evt); + } + + if( typeof(gpinputs[cmd]) === 'function' ){ + console.log('gpinputs is deprecated as of 3.6'); + return gpinputs[cmd].call(this, evt, evt); //evt twice so the same function can be used for gplinks and gpinputs + } + + switch(cmd){ + case 'gppost': + case 'gpajax': + evt.preventDefault(); + return $gp.post(this); + } + + return true; + }); + + + //expanding menus + $document.on('mouseenter', '.expand_child', function(){ + var $this = $(this).addClass('expand'); + if( $this.hasClass('simple_top') ){ + $this.addClass('simple_top_hover'); + } + }).on('mouseleave', '.expand_child', function(){ + $(this).removeClass('expand simple_top_hover'); + }); + + + /** + * Handle all clicks on tags + * Use of name and rel attributes is deprecated as of gpEasy 3.6 + * + */ + $document.on('click', 'a', function(evt){ + + var $this = $(this); + var cmd = $this.data('cmd'); + var arg = $this.data('arg'); + if( !cmd ){ + // deprecated 3.6 + cmd = $this.attr('name'); + arg = $this.attr('rel'); + } + + if( $this.hasClass('gpconfirm') && !confirm(this.title) ){ + evt.preventDefault(); + return; + } + + if( typeof($gp.links[cmd]) === 'function' ){ + return $gp.links[cmd].call(this, evt, arg); + } + + // @deprecated 3.6 + if( typeof(gplinks[cmd]) === 'function' ){ + console.log('gplinks is deprecated as of 3.6'); + return gplinks[cmd].call(this, arg, evt); + } + + switch(cmd){ + + case 'toggle_show': + $(arg).toggle(); + break; + + case 'inline_box': + $gp.CopyVals(arg,this); + $(this).colorbox( + $gp.cboxSettings({ + inline : true, + href : arg, + open : true + }) + ); + break; + + case 'postlink': + $gp.post_link(this); + break; + + case 'gpajax': + $gp.jGoTo(this.href, this); + break; + + case 'creq': + $gp.cGoTo(this, true); + break; + + case 'cnreq': + $gp.cGoTo(this, false); + break; + + case 'close_message': + $this.closest('div').slideUp(); + break; + + case 'copy_message': + var msg_text = $this.closest('div').find('ul') + .get(0).innerText; // innerText preserves line breaks + var $tmp = $(''; + + \gp\tool\Plugins::Action('contact_form_pre_captcha'); + + if( !$this->sent && \gp\tool\Recaptcha::isActive() ){ + echo '
    '; + echo \gp\tool\Output::ReturnText('captcha'); + \gp\tool\Recaptcha::Form(); + echo '
    '; + } + + if( $this->sent ){ + echo '
    '; + echo \gp\tool\Output::ReturnText('message_sent', '%s', 'message_sent'); + echo '
    '; + }else{ + echo ''; + $key = 'send_message'; + $text = \gp\tool\Output::SelectText($key); + + if( \gp\tool\Output::ShowEditLink('Admin_Theme_Content') ){ + $query = 'cmd=EditText&key=' . urlencode($key); + echo \gp\tool\Output::EditAreaLink( + $edit_index, + 'Admin_Theme_Content/Text', + $langmessage['edit'], + $query, + ' title="' . $key . '" data-cmd="gpabox" ' + ); + echo ''; + }else{ + echo ''; + } + } + + echo ''; + echo '
    '; + } +} diff --git a/include/special/Galleries.php b/include/special/Galleries.php new file mode 100644 index 0000000..5d8aafd --- /dev/null +++ b/include/special/Galleries.php @@ -0,0 +1,296 @@ +galleries = self::GetData(); + $this->GenerateOutput(); + } + + + /** + * Determine if the gallery page is hidden or deleted + * + */ + public function GalleryVisible( $title, $info ){ + global $gp_index, $gp_menu, $gp_titles; + + if( !isset($gp_index[$title]) ){ + unset($this->galleries[$title]); + $this->title_removed = true; + return false; + } + + $index = $gp_index[$title]; + $title_info = $gp_titles[$index]; + + if( (isset($info['visibility']) && $info['visibility'] == 'hide') || isset($title_info['vis']) ){ + $this->not_visible[$title] = $info; + return false; + } + + return true; + } + + // save the galleries index file + public function PostSave(){ + if( !$this->title_removed ){ + return; + } + self::SaveIndex($this->galleries); + } + + + + /** + * Get Gallery Index + * + * @static + */ + public static function GetData(){ + + $galleries = \gp\tool\Files::Get('_site/galleries'); + if( !$galleries ){ + return array(); + } + + if( version_compare(\gp\tool\Files::$last_version,'2.2','<=') ){ + self::UpdateData($galleries); + } + + return $galleries; + } + + /** + * Add visibility settings according to old method for handling gallery visibility + * @static + */ + public static function UpdateData(&$galleries){ + global $gp_index, $gp_menu; + + foreach($galleries as $title => $info){ + + if( isset($info['visibility']) ){ + continue; + } + + $id = $gp_index[$title]; + + if( !isset($gp_menu[$id]['level']) ){ + $galleries[$title]['visibility'] = 'hide'; + }else{ + $galleries[$title]['visibility'] = 'show'; + } + } + } + + + public function GenerateOutput(){ + global $langmessage; + + \gp\tool::ShowingGallery(); + + echo '
    '; + echo '

    '; + echo \gp\tool\Output::ReturnText('galleries'); + echo '

    '; + + $wrap = \gp\admin\Tools::CanEdit($this->page->gp_index); + if( $wrap ){ + echo \gp\tool\Output::EditAreaLink($edit_index,'Admin/Galleries',$langmessage['edit']); + echo '
    '; // class="edit_area" added by javascript + } + + + $image_text = \gp\tool\Output::ReturnText('image'); + $images_text = \gp\tool\Output::ReturnText('images'); + + $list = ''; + $shown = 0; + foreach($this->galleries as $title => $info ){ + + + //page is hidden + if( !$this->GalleryVisible($title,$info) ){ + continue; + } + + $count = ''; + if( is_array($info) ){ + $icon = $info['icon']; + if( $info['count'] == 1 ){ + $count = $info['count'].' '.\gp\tool\Output::ReturnText('image'); + }elseif( $info['count'] > 1 ){ + $count = $info['count'].' '.\gp\tool\Output::ReturnText('images'); + } + }else{ + $icon = $info; + } + + if( empty($icon) ){ + continue; + } + + + $icon = rawurldecode($icon); //prevent double encoding + if( strpos($icon,'/thumbnails/') === false ){ + $thumbPath = \gp\tool::GetDir('/data/_uploaded/image/thumbnails'.$icon.'.jpg'); + }else{ + $thumbPath = \gp\tool::GetDir('/data/_uploaded'.$icon); + } + + $label = \gp\tool::GetLabel($title); + $title_attr = ' title="'.\gp\tool::GetBrowserTitle($title).'"'; + $label_img = ' '; + + $list .= '
  • ' + . \gp\tool::Link($title,$label_img,'',$title_attr) + . '
    ' + . \gp\tool::Link($title, $label,'',$title_attr) + . '

    ' + .$count + .'

    ' + .'
    ' + .'
  • '; + } + + if( !empty($list) ){ + echo ''; + } + + if( $wrap ){ + echo '
    '; + } + $this->PostSave(); + + echo '
    '; + } + + /* + + Updating Functions + + The galleries.php file needs to be updated when changes are made to pages with galleries + When a page is... + ... renamed: RenamedGallery() + ... edited: UpdateGalleryInfo() + ... added: do nothing, there won't be any images yet, wait till edited + ... deleted: RemovedGallery() + ... restored from trash: UpdateGalleryInfo() via RestoreFile() in gp\admin\Content\Trash + */ + + + + /** + * Extract information about the gallery from it's html: img_count, icon_src + * + * @static + */ + public static function UpdateGalleryInfo($title,$file_sections){ + + $content = ''; + $has_gallery = false; + foreach($file_sections as $section_data){ + if( $section_data['type'] == 'gallery' ){ + $content .= $section_data['content']; + $has_gallery = true; + } + } + + if( !$has_gallery ){ + self::RemovedGallery($title); + return; + } + + $new_count = preg_match_all('#(rel|class)="gallery_gallery"#',$content,$matches); + + //first image + $new_icon = ''; + $first_img = preg_match('#]*src="([^>"]*)"[^>]*>#',$content,$match); //uploaded file's names are stripped of " and > + if( $first_img === 1 ){ + $new_icon = $match[1]; + + $pos = strpos($new_icon,'/data/_uploaded'); + if( $pos !== false ){ + $new_icon = substr($new_icon,$pos+15); + } + } + + + $galleries = self::GetData(); + + $orig_icon = $orig_count = false; + $orig_info = array(); + if( isset($galleries[$title]) && is_array($galleries[$title]) ){ + $orig_info = $galleries[$title]; + $orig_icon = $orig_info['icon']; + $orig_count = $orig_info['count']; + } + + if( ($orig_icon == $new_icon ) && ($orig_count == $new_count) ){ + return; + } + + $orig_info['icon'] = $new_icon; + $orig_info['count'] = $new_count; + $galleries[$title] = $orig_info; + self::SaveIndex($galleries); + } + + + /** + * Handle the removal of a gallery page for \gp\admin\Menu\Tools.php + * + */ + public static function RemovedGallery($title){ + + $galleries = self::GetData(); + if( !isset($galleries[$title]) ){ + return; + } + + unset($galleries[$title]); + self::SaveIndex($galleries); + } + + + /** + * Handle the renaming of galleries for \gp\admin\Menu\Tools.php + * + * @static + * + */ + public static function RenamedGallery($old_title,$new_title){ + + $galleries = self::GetData(); + if( !isset($galleries[$old_title]) ){ + return; + } + + if( \gp\tool\Files::ArrayInsert($old_title,$new_title,$galleries[$old_title],$galleries,0,1) ){ + self::SaveIndex($galleries); + } + } + + public static function SaveIndex($galleries){ + global $dataDir; + + $file = $dataDir.'/data/_site/galleries.php'; + return \gp\tool\Files::SaveData($file,'galleries',$galleries); + } + + +} diff --git a/include/special/Map.php b/include/special/Map.php new file mode 100644 index 0000000..e048840 --- /dev/null +++ b/include/special/Map.php @@ -0,0 +1,91 @@ +xml(); + return; + } + + $this->MultiSiteData(); + + echo '
    '; + echo '
    '; + echo \gp\tool::Link('Special_Site_Map','XML','xml'); + echo '
    '; + echo '

    '; + echo \gp\tool\Output::ReturnText('site_map'); + echo '

    '; + + \gp\tool\Output::GetFullMenu(); + + echo '
    '; + + } + + function MultiSiteData(){ + global $config; + + $this->page->head .= ''; + + if( defined('multi_site_unique') ){ + $this->page->head .= ''; + } + if( defined('service_provider_id') && is_numeric(service_provider_id) ){ + $this->page->head .= ''; + } + } + + + /* + + http://www.example.com/ + 2005-01-01 + monthly + 0.8 + + */ + function xml(){ + global $gp_menu, $gp_titles; + + header('Content-Type: text/xml; charset=UTF-8'); + echo ''; + echo ''; + + + foreach($gp_menu as $key => $info){ + $title = \gp\tool::IndexToTitle($key); + $gptitle = $gp_titles[$key]; + + if( isset($info['level']) + && !isset( $gptitle['vis']) + && !( isset($gptitle['rel']) && strpos($gptitle['rel'], 'noindex') !== false ) ) { + echo "\n"; + echo ''; + echo ''; + echo isset($info['url']) ? $info['url'] : \gp\tool::AbsoluteUrl($title,'',true,'',true); + echo ''; + echo ''; + } + } + + echo ''; + + + die(); + } +} diff --git a/include/special/Missing.php b/include/special/Missing.php new file mode 100644 index 0000000..6ecabba --- /dev/null +++ b/include/special/Missing.php @@ -0,0 +1,304 @@ +datafile = '_site/error_data'; + $this->error_data = \gp\tool\Files::Get($this->datafile,'error_data'); + + $this->error_data += array( + 'redirects' => array( + ) + ); + + } + + public function __construct($args){ + global $langmessage; + + parent::__construct($args); + $this->Init(); + + $this->requested = $this->page->requested; + } + + public function RunScript(){ + $cmd = \gp\tool::GetCommand(); + $this->RunCommands($cmd); + } + + public function DefaultDisplay(){ + + if( $this->page->gp_index !== 'special_missing' ){ + $this->CheckRedirect(); + $this->CheckSimilar(); + } + + $this->Get404(); + } + + /** + * Redirect the request if a redirection entry matches the requested page + * + */ + public function CheckRedirect(){ + + if( is_null($this->requested) ){ + return; + } + + $parts = explode('/',$this->requested); + $first_part = array_shift($parts); + + if( !isset($this->error_data['redirects'][$first_part]) ){ + return; + } + + + $target = $this->error_data['redirects'][$first_part]['target']; + + $target = $this->GetTarget($target); + + if( $target === false ){ + return; + } + + if( !empty($parts) && is_string($target) ){ + $target .= '/'.implode('/',$parts); + } + + $code = $this->error_data['redirects'][$first_part]['code']; + \gp\tool::Redirect($target,$code); + } + + /** + * Redirect the request if the requested page closely matches an existing page + * If it's just a difference of case, then the similarity will be 100% + * + */ + public function CheckSimilar(){ + global $config; + + $requested = trim($this->requested,'/'); + $similar = $this->SimilarTitleArray($requested); + $first_title = key($similar); + $first_percent = current($similar); + + if( $config['auto_redir'] > 0 && $first_percent >= $config['auto_redir'] ){ + $redirect = \gp\tool::GetUrl($first_title,http_build_query($_GET),false); + \gp\tool::Redirect($redirect); + } + } + + + + /** + * Translate the $target url to a url that can be used with Header() or in a link + * + * @param string $target The user supplied value for redirection + * @param boolean $get_final If true, GetTarget() will check for additional redirection and $target existence before returning the url. Maximum of 10 redirects. + * @return string|false + */ + public function GetTarget($target,$get_final = true){ + global $gp_index; + static $redirects = 0; + + if( empty($target) ){ + return \gp\tool::GetUrl(''); + } + + if( !$this->isGPLink($target) ){ + return $target; + } + + if( !$get_final ){ + return \gp\tool::GetUrl($target); + } + + + //check for more redirects + if( isset($this->error_data['redirects'][$target]) ){ + $redirects++; + if( $redirects > 10 ){ + return false; + } + + $target = $this->error_data['redirects'][$target]['target']; + return $this->GetTarget($target); + } + + + //check for target existence + if( isset($gp_index[$target]) ){ + return \gp\tool::GetUrl($target); + } + + $scripts = \gp\admin\Tools::AdminScripts(); + if( isset($scripts[$target]) ){ + return \gp\tool::GetUrl($target); + } + + return false; + } + + public function isGPLink($target){ + //has a url scheme (aka protocol) + $reg = '#^[a-zA-Z][a-zA-Z0-9\+\.\-]+:#'; + if( preg_match($reg,$target,$matches) ){ + return false; + } + + //strings beginning with / could be gplinks, they could also links to non-cms managed pages + // we could do additional testing, but we could never be certain what the user intent is + if( strpos($target,'/') === 0 ){ + return false; + } + + return true; + } + + + + public function Get404(){ + global $langmessage,$page; + + \gp\tool\Output::AddHeader('Not Found',true,404); + $page->head .= ''; //this isn't getting to the template because $page isn't available yet + + //message for admins + if( \gp\tool::LoggedIn() ){ + if( $this->requested && \gp\tool::SpecialOrAdmin($this->requested) === false ){ + $with_spaces = htmlspecialchars($this->requested); + $link = \gp\tool::GetUrl('Admin/Menu/Ajax','cmd=AddHidden&redir=redir&title='.rawurlencode($this->requested)).'" title="'.$langmessage['create_new_file'].'" data-cmd="gpabox'; + $message = sprintf($langmessage['DOESNT_EXIST'],$with_spaces,$link); + msg($message); + } + } + + echo '
    '; + //Contents of 404 page + $wrap = \gp\tool\Output::ShowEditLink('Admin/Missing'); + if( $wrap ){ + echo \gp\tool\Output::EditAreaLink($edit_index,'Admin/Missing',$langmessage['edit'],'cmd=edit404',' title="'.$langmessage['404_Page'].'" '); + echo '
    '; // class="edit_area" added by javascript + } + + echo self::Get404Output(); + + if( $wrap ){ + echo '
    '; + } + echo '
    '; + } + + /** + * Return the custom 404 page content if it exists, otherwise return the default content + * + */ + public function Get404Output(){ + + if( isset($this->error_data['404_TEXT']) ){ + $text = $this->error_data['404_TEXT']; + }else{ + $text = self::DefaultContent(); + } + + return str_replace('{{Similar_Titles}}',$this->SimilarTitles(),$text); + } + + /** + * Get a comma separated list of links to titles similar to the requested page + * @return string + * + */ + public function SimilarTitles(){ + + $similar = $this->SimilarTitleArray($this->requested); + $similar = array_slice($similar,0,7,true); + $result = ''; + + foreach($similar as $title => $percent_similar){ + if ( $title === key($similar) || $percent_similar > 10 ) { + $result .= \gp\tool::Link_Page($title).', '; + } + } + + return rtrim($result,', '); + } + + + /** + * Get a list of existing titles similar to the requested page + * @param string $title of requested page + * @return array of similar titles reverse-numerical sorted by similarity (percent) + * + */ + public function SimilarTitleArray($title){ + global $gp_index, $gp_titles; + + $similar_titles = []; + $lower = str_replace(' ','_',strtolower($title)); + + foreach($gp_index as $title => $index){ + + //skip 'special_missing' page + if( $index == 'special_missing' ){ + continue; + } + + //skip private pages when not logged-in + if( !\gp\tool::LoggedIn() && isset($gp_titles[$index]['vis']) ){ + continue; + } + + similar_text($lower,mb_strtolower($title),$percent); + $similar_titles[$index] = [ + 'title' => $title, + 'percent' => $percent, + ]; + } + + uasort($similar_titles, function($a, $b) { + return $b['percent'] - $a['percent']; + }); + + $similar_titles = \gp\tool\Plugins::Filter('SimilarTitles', [$similar_titles]); + + $similar = []; + foreach( $similar_titles as $similar_title ){ + $similar[$similar_title['title']] = $similar_title['percent']; + } + + return $similar; + } + + + /** + * Returnt the default content of the 404 page + * + */ + public function DefaultContent(){ + global $langmessage; + $text = '

    '.$langmessage['Not Found'].'

    '; + $text .= '

    '; + $text .= $langmessage['OOPS_TITLE']; + $text .= '

    '; + $text .= '

    '; + $text .= ''.$langmessage['One of these titles?'].''; + $text .= '

    {{Similar_Titles}}
    '; + $text .= '

    '; + return $text; + } + +} diff --git a/include/special/Page.php b/include/special/Page.php new file mode 100644 index 0000000..3a3ecff --- /dev/null +++ b/include/special/Page.php @@ -0,0 +1,217 @@ +requested = $title; + $this->title = $title; + $this->lang = $config['language']; + if( isset($languages[$this->lang]) ){ + $this->language = $languages[$this->lang]; + } + } + + public function RunScript(){ + global $gp_index, $langmessage; + + $scriptinfo = self::GetScriptInfo($this->title); + if( $scriptinfo === false ){ + + switch($this->title){ + case 'Special_ExtraJS'; + $this->ExtraJS(); + //dies + } + + + $this->Error_404(); + return; + } + + $this->gp_index = $gp_index[$this->title]; + $this->TitleInfo = $scriptinfo; + + if( !$this->CheckVisibility() ){ + return false; + } + + //allow addons to affect page actions and how a page is displayed + $cmd = \gp\tool::GetCommand(); + $cmd_after = \gp\tool\Plugins::Filter('PageRunScript',array($cmd)); + if( $cmd !== $cmd_after ){ + $cmd = $cmd_after; + if( $cmd === 'return' ){ + return; + } + } + + if( \gp\tool::LoggedIn() && \gp\admin\Tools::HasPermission('Admin_Menu') ){ + $this->cmds['RenameForm'] = '\\gp\\Page\\Rename::RenameForm'; + $this->cmds['RenameFile'] = '\\gp\\Page\\Rename::RenamePage'; + $this->cmds['ToggleVisibility'] = array('\\gp\\Page\\Visibility::TogglePage','DefaultDisplay'); + $this->cmds['ManageSections'] = '\gp\Page\Edit::ManageSections'; + } + + $this->RunCommands($cmd); + } + + public function DefaultDisplay(){ + $this->contentBuffer = self::ExecInfo($this->TitleInfo); + } + + public static function ExecInfo($scriptinfo ){ + ob_start(); + \gp\tool\Output::ExecInfo($scriptinfo); + return ob_get_clean(); + } + + + + /** + * Generate admin toolbar links + * + */ + public function AdminLinks(){ + global $langmessage, $config; + + $admin_links = $this->admin_links; + + + // HideAdminUI + array_unshift( + $admin_links, + \gp\tool::Link( + $this->title, + '', + '', + [ + 'title' => $langmessage['Hide Admin UI'], + 'class' => 'admin-link admin-link-hide-ui', + 'data-cmd' => 'hide_ui', + ] + ) + ); + + + $menu_permissions = \gp\admin\Tools::HasPermission('Admin_Menu'); + + // page options: less frequently used links that don't have to do with editing the content of the page + $option_links = array(); + if( $menu_permissions ){ + $option_links[] = \gp\tool::Link($this->title,$langmessage['rename/details'],'cmd=renameform&index='.urlencode($this->gp_index),'data-cmd="gpajax"'); + $option_links[] = \gp\tool::Link('Admin/Menu',$langmessage['current_layout'],'cmd=layout&from=page&index='.urlencode($this->gp_index),array('title'=>$langmessage['current_layout'],'data-cmd'=>'gpabox')); + } + + if( \gp\admin\Tools::HasPermission('Admin_User') ){ + $option_links[] = \gp\tool::Link('Admin/Permissions',$langmessage['permissions'],'index='.urlencode($this->gp_index),array('title'=>$langmessage['permissions'],'data-cmd'=>'gpabox')); + } + + if( $menu_permissions ){ + $option_links[] = \gp\Page\Edit::ToggleVisibilityLink($this->gp_index, $this->visibility != 'private'); + } + + if( !empty($option_links) ){ + $admin_links[$langmessage['options']] = $option_links; + } + + + return $admin_links; + } + + + /** + * + */ + public static function GetScriptInfo(&$requested,$redirect=true){ + global $dataDir,$gp_index,$gp_titles; + + $scripts['special_site_map']['class'] = '\\gp\\special\\Map'; + + $scripts['special_galleries']['class'] = '\\gp\\special\\Galleries'; + + $scripts['special_contact']['class'] = '\\gp\\special\\Contact'; + + $scripts['special_missing'] = array( 'class' => '\\gp\\special\\Missing', + 'method' => 'RunScript', + ); + + $scripts['special_gpsearch']['class'] = '\\gp\\special\\Search'; + + //check for use of a index instead of a page title + $translated = \gp\tool::SpecialHref($requested); + if( $translated != $requested ){ + $requested = $translated; + if( $redirect ){ + $title = \gp\tool::GetUrl($requested,http_build_query($_GET),false); + \gp\tool::Redirect($title); + } + } + + + //get the script info + $parts = explode('/',$requested); + do{ + $requested = implode('/',$parts); + if( isset($gp_index[$requested]) ){ + + $index = $gp_index[$requested]; + // Merge page data & script data if both exist + if( isset($scripts[$index]) && isset($gp_titles[$index])){ + return array_merge($scripts[$index], $gp_titles[$index]); + } + if( isset($scripts[$index]) ){ + return $scripts[$index]; + } + + if( isset($gp_titles[$index]) ){ + return $gp_titles[$index]; + } + } + array_pop($parts); + }while( count($parts) ); + + return false; + } + + + public function ExtraJS(){ + header('Content-type: application/javascript'); + + trigger_error('Deprecated: special_extrajs'); + + $_GET += array('which'=>array()); + + foreach((array)$_GET['which'] as $which_code){ + + switch($which_code){ + + case 'autocomplete2': + $options['admin_vals'] = false; + $options['var_name'] = 'gp_include_titles'; + echo \gp\tool\Editing::AutoCompleteValues(false,$options); + break; + + case 'autocomplete': + echo \gp\tool\Editing::AutoCompleteValues(true); + break; + + case 'gp_ckconfig': + $options = array(); + echo \gp\tool\Editing::CKConfig($options,'gp_ckconfig'); + break; + } + } + + die(); + } + +} diff --git a/include/special/Search.php b/include/special/Search.php new file mode 100644 index 0000000..8747631 --- /dev/null +++ b/include/special/Search.php @@ -0,0 +1,618 @@ +config_file = $dataDir . '/data/_site/config_search.php'; + $this->GetConfig(); + + if( $this->Admin() ){ + return; + } + + //admin popup or visitor + $_REQUEST += array('q' => ''); + if( \gp\tool::LoggedIn() && isset($_REQUEST['gpx_content']) && $_REQUEST['gpx_content'] == 'gpabox' ){ + $this->AdminSearch(); + }else{ + $this->Search(); + } + } + + + + public function AdminSearch(){ + global $langmessage; + + $this->gpabox = true; + $this->show_stats = true; + $this->search_hidden = true; + + echo '
    '; + echo ''; + echo '
    '; + } + + + + public function Search(){ + global $langmessage; + + $placeholder = $langmessage['Search'] . '…'; + $custom_text = \gp\tool\Output::SelectText('Search'); + if( $custom_text != '' ){ + $placeholder = htmlspecialchars($custom_text) . '…'; + } + + echo ''; + } + + + + public static function Gadget(){ + global $langmessage; + + $query = ''; + if( isset($_GET['q']) ){ + $query = $_GET['q']; + } + + $placeholder = $langmessage['Search'] . '…'; + $custom_text = \gp\tool\Output::SelectText('Search'); + if( $custom_text != '' ){ + $placeholder = htmlspecialchars($custom_text) . '…'; + } + + echo '

    '; + echo \gp\tool\Output::GetAddonText('Search'); + echo '

    '; + echo '
    '; + echo '
    '; + echo ''; + echo ''; + + $html = ''; + echo \gp\tool\Output::GetAddonText('Search', $html); + + echo '
    '; + echo '
    '; + } + + + + public function RunQuery(){ + + if( !empty($_REQUEST['q']) ){ + $this->SearchPattern(); + $this->SearchPages(); + \gp\tool\Plugins::Action('Search', array($this)); + } + + $this->ShowResults(); + } + + + + public function ShowResults(){ + global $langmessage; + + if( !count($this->results) ){ + if( !empty($_REQUEST['q']) ){ + echo '

    '; + echo \gp\tool\Output::GetAddonText($langmessage['search_no_results']); + echo '

    '; + } + return; + } + + $this->RemoveDups(); + usort( $this->results, array($this, 'sort') ); + + $total = count($this->results); + $len = 20; + $total_pages = ceil($total/$len); + $current_page = self::ReqPage('pg', $total_pages ); + + $start = $current_page * $len; + $end = min($start+$len, $total); + + $this->results = array_slice($this->results, $start, $len, true); + echo '

    '; + echo sprintf($langmessage['SHOWING'], ($start + 1), $end, $total); + echo '

    '; + + echo '
    '; + foreach($this->results as $result){ + echo '

    '; + echo isset($result['link']) ? $result['link'] : \gp\tool::Link( + $result['slug'], + $result['label'], + 'highlight=' . rawurlencode($_REQUEST['q']) + ); // $result['query'] + + if( $this->show_stats ){ + echo ' ' . $result['matches'] . '' ; + } + echo '

    '; + + echo $result['content']; + + if( $this->show_stats ){ + echo ' '; + echo $result['matches'] . ' match(es) out of ' . $result['words'] . ' words '; + echo ' '; + } + echo '
    '; + } + echo '
    '; + + $attr = 'class="page-link"'; + if( $this->gpabox ){ + $attr .= ' data-cmd="gpabox"'; + echo '
    '; + } + + $query = 'q=' . rawurlencode($_REQUEST['q']); + self::PaginationLinks($current_page, $total_pages, 'special_gpsearch', $query, 'pg', $attr); + } + + + + /** + * Get the requested page number + * + * @param string $key + * @param int $total_pages + */ + public static function ReqPage($key='pg', $total_pages=null){ + + if( isset($_REQUEST[$key]) ){ + + $pg = (int)$_REQUEST[$key]; + + if( !is_null($total_pages) && $total_pages > 0 ){ + $pg = min($pg, $total_pages - 1); + } + + return max(0,$pg); + } + + return 0; + } + + + + /** + * Pagination links + * + */ + public static function PaginationLinks($current_page, $total_pages, $slug, $query, $page_key='pg', $attr=''){ + global $langmessage; + + if( $total_pages < 1 ){ + return; + } + + $prev_text = $langmessage['Previous']; + $next_text = $langmessage['Next']; + $current_text = $langmessage['Current Page']; + + echo ''; + } + + + + public static function PaginationLink($slug, $label, $query, $page_key, $attr, $page){ + + if( $page > 0){ + $query .= '&' . $page_key . '=' . $page; + } + + echo '
  • ' . \gp\tool::Link($slug, $label, $query, $attr) . '
  • '; + } + + + + /** + * Remove duplicate matches + * + */ + public function RemoveDups(){ + $links = array(); + foreach($this->results as $key => $result){ + + $link = isset($result['url']) ? $result['url'] : \gp\tool::GetUrl($result['slug'], $result['query']); + // $link = mb_strtolower($link); + + if( in_array($link,$links) ){ + unset($this->results[$key]); + }else{ + $links[] = $link; + } + } + } + + + + public function Sort($resulta,$resultb){ + return $resulta['strength'] < $resultb['strength']; + } + + + + public function SearchPattern(){ + // $query = mb_strtolower($_REQUEST['q']); + $query = $_REQUEST['q']; + // Search for the exact query when it is doubled quoted + if (substr($query, 0, 1) == '"' && substr($query, -1) == '"') { + $query = substr($query, 1, -1); + $words = array($query); + } else { + preg_match_all("/\S+/", $query, $words); + $words = array_unique($words[0]); + } + + $sub_pattern1 = $sub_pattern2 = array(); + foreach($words as $word){ + $sub_pattern1[] = '\b' . preg_quote($word, '#') . '\b'; + $sub_pattern2[] = preg_quote($word, '#'); + } + + $this->search_pattern = '#(?:(' . implode('|', $sub_pattern1) . ')|(' . implode('|', $sub_pattern2) . '))#Siu'; + } + + + + public function Admin(){ + global $langmessage; + + if( !\gp\tool::LoggedIn() ){ + return false; + } + $this->page->admin_links[] = array( + 'special_gpsearch', + $langmessage['configuration'], + 'cmd=config', + [ 'data-cmd' => 'gpabox', 'class' => 'admin-link-search-config' ] + ); + $cmd = \gp\tool::GetCommand(); + + switch($cmd){ + + case 'save_config': + if( $this->SaveConfig() ){ + break; + } + return true; + + case 'config': + $this->Config($this->search_config); + return true; + + } + return false; + } + + + + /** + * Get the search configuration + * + */ + public function GetConfig(){ + $this->search_config = \gp\tool\Files::Get($this->config_file, 'search_config'); + $this->search_config += array('search_hidden' => false); + } + + + + public function SaveConfig(){ + global $langmessage; + + if( isset($_POST['search_hidden']) ){ + $search_config['search_hidden'] = true; + }else{ + $search_config['search_hidden'] = false; + } + + if( \gp\tool\Files::SaveData($this->config_file, 'search_config', $search_config) ){ + msg($langmessage['SAVED']); + $this->search_config = $search_config; + return true; + } + + msg($langmessage['OOPS']); + $this->Config($_POST); + return false; + } + + + + public function Config($array=array()){ + global $langmessage, $addonFolderName, $gp_index; + + echo '

    ' . $langmessage['Search'] . ' » ' . $langmessage['configuration'] . '

    '; + + echo '
    '; + + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
    ' . $langmessage['options'] . '' . $langmessage['Value'] . '' . $langmessage['default'] . '
    ' . $langmessage['Search Hidden Files'] . ''; + $checked = isset($array['search_hidden']) && $array['search_hidden'] ? ' checked="checked"' : ''; + echo ''; + echo '' . $langmessage['False'] . '
    '; + + echo '

    '; + echo ''; + echo ' '; + echo ' '; + echo '

    '; + + echo '
    '; + } + + + + public function SearchPages(){ + global $gp_index; + + ob_start(); + foreach($gp_index as $title => $index){ + if( \gp\tool::SpecialOrAdmin($title) === false ){ + $this->SearchPage($title, $index); + } + } + ob_get_clean(); + } + + + + public function SearchPage($title, $index){ + global $gp_menu, $gp_titles; + + //search hidden? + if( !$this->search_hidden && !isset($gp_menu[$index]) ){ + return; + } + + //private pages + if( !\gp\tool::LoggedIn() && isset($gp_titles[$index]['vis']) ){ + return; + } + + $full_path = \gp\tool\Files::PageFile($title); + $file_sections = \gp\tool\Files::Get($full_path, 'file_sections'); + + if( !$file_sections ){ + return; + } + + $content = \gp\tool\Output\Sections::Render($file_sections, $title, \gp\tool\Files::$last_stats); + $label = \gp\tool::GetLabel($title); + + $this->FindString($content, $label, $title); + } + + + + public function FindString(&$content, $label, $slug, $link_query=''){ + $this->search_count++; + + //search all of the content include html + // $content = mb_strtolower($content); + $content = $label . ' ' . $content; + $match_count = preg_match_all($this->search_pattern, $content, $matches, PREG_OFFSET_CAPTURE); + if( $match_count < 1 ){ + return; + } + $words = str_word_count($content); + $strength = $this->Strength($matches,$words); + + //format content, remove html + $label_len = strlen($label); + $content = preg_replace('#]*>.*?#si', '', $content); + $content = preg_replace('#]*>.*?#si', '', $content); + $content = substr($content, $label_len); + $content = str_replace('>', '> ', $content); + $content = preg_replace('/\s+/', ' ', $content); + $content = strip_tags($content); + preg_match($this->search_pattern, $content, $matches, PREG_OFFSET_CAPTURE); + $start = isset($matches[0][1]) ? $matches[0][1] : 0; + + //find a space at the beginning to start from + $i = 0; + do{ + $i++; + $start_offset = $i * 10; + $start = max(0, $start - $start_offset); + $trimmed = substr($content, $start, 300); + $space = strpos($trimmed, ' '); + if( $space < $start_offset ){ + $content = substr($trimmed, $space); + break; + } + }while( ($start-$start_offset) > 0); + + + //find a space at the end + if( strlen($content) > 250 ){ + $space2 = strpos($content, ' ', $space + 220); + if( $space2 > 0 ){ + $content = substr($content, 0, $space2); + } + } + + $result = array(); + $result['label'] = $label; + $result['slug'] = $slug; + $result['query'] = $link_query; + $result['content'] = preg_replace($this->search_pattern, '\1\2', $content); + $result['words'] = $words; + $result['matches'] = $match_count; + $result['strength'] = $strength; + $this->results[] = $result; + } + + + + function Strength($matches, $len){ + + //space around search terms + $factor = 0; + foreach((array)$matches[1] as $match){ + if( is_array($match) && $match[1] >= 0 ){ + $factor += 3; + } + } + + //no space around search term + foreach((array)$matches[2] as $match){ + if( is_array($match) && $match[1] >= 0 ){ + $factor += 1; + } + } + + $strength = $factor / $len; + return round($strength, 8); + } + +} diff --git a/include/thirdparty/Bootstrap/css/bootstrap-responsive.min.css b/include/thirdparty/Bootstrap/css/bootstrap-responsive.min.css new file mode 100644 index 0000000..d1b7f4b --- /dev/null +++ b/include/thirdparty/Bootstrap/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/include/thirdparty/Bootstrap/css/bootstrap.min.css b/include/thirdparty/Bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000..c10c7f4 --- /dev/null +++ b/include/thirdparty/Bootstrap/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/include/thirdparty/Bootstrap/img/glyphicons-halflings-white.png b/include/thirdparty/Bootstrap/img/glyphicons-halflings-white.png new file mode 100644 index 0000000..3bf6484 Binary files /dev/null and b/include/thirdparty/Bootstrap/img/glyphicons-halflings-white.png differ diff --git a/include/thirdparty/Bootstrap/img/glyphicons-halflings.png b/include/thirdparty/Bootstrap/img/glyphicons-halflings.png new file mode 100644 index 0000000..a996999 Binary files /dev/null and b/include/thirdparty/Bootstrap/img/glyphicons-halflings.png differ diff --git a/include/thirdparty/Bootstrap/js/bootstrap-affix.js b/include/thirdparty/Bootstrap/js/bootstrap-affix.js new file mode 100644 index 0000000..827ff45 --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-affix.js @@ -0,0 +1,117 @@ +/* ========================================================== + * bootstrap-affix.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#affix + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* AFFIX CLASS DEFINITION + * ====================== */ + + var Affix = function (element, options) { + this.options = $.extend({}, $.fn.affix.defaults, options) + this.$window = $(window) + .on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this)) + this.$element = $(element) + this.checkPosition() + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + , scrollTop = this.$window.scrollTop() + , position = this.$element.offset() + , offset = this.options.offset + , offsetBottom = offset.bottom + , offsetTop = offset.top + , reset = 'affix affix-top affix-bottom' + , affix + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top() + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + + affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? + false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? + 'bottom' : offsetTop != null && scrollTop <= offsetTop ? + 'top' : false + + if (this.affixed === affix) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? position.top - scrollTop : null + + this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) + } + + + /* AFFIX PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.affix + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('affix') + , options = typeof option == 'object' && option + if (!data) $this.data('affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + $.fn.affix.defaults = { + offset: 0 + } + + + /* AFFIX NO CONFLICT + * ================= */ + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + /* AFFIX DATA-API + * ============== */ + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + , data = $spy.data() + + data.offset = data.offset || {} + + data.offsetBottom && (data.offset.bottom = data.offsetBottom) + data.offsetTop && (data.offset.top = data.offsetTop) + + $spy.affix(data) + }) + }) + + +}(window.jQuery); \ No newline at end of file diff --git a/include/thirdparty/Bootstrap/js/bootstrap-alert.js b/include/thirdparty/Bootstrap/js/bootstrap-alert.js new file mode 100644 index 0000000..8917f94 --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-alert.js @@ -0,0 +1,99 @@ +/* ========================================================== + * bootstrap-alert.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT + * ================= */ + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + /* ALERT DATA-API + * ============== */ + + $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}(window.jQuery); \ No newline at end of file diff --git a/include/thirdparty/Bootstrap/js/bootstrap-button.js b/include/thirdparty/Bootstrap/js/bootstrap-button.js new file mode 100644 index 0000000..66df0a2 --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-button.js @@ -0,0 +1,105 @@ +/* ============================================================ + * bootstrap-button.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT + * ================== */ + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + /* BUTTON DATA-API + * =============== */ + + $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + +}(window.jQuery); \ No newline at end of file diff --git a/include/thirdparty/Bootstrap/js/bootstrap-carousel.js b/include/thirdparty/Bootstrap/js/bootstrap-carousel.js new file mode 100644 index 0000000..b40edd7 --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-carousel.js @@ -0,0 +1,207 @@ +/* ========================================================== + * bootstrap-carousel.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + if (this.interval) clearInterval(this.interval); + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , getActiveIndex: function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + return this.$items.index(this.$active) + } + + , to: function (pos) { + var activeIndex = this.getActiveIndex() + , that = this + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activeIndex == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + e = $.Event('slide', { + relatedTarget: $next[0] + , direction: direction + }) + + if ($next.hasClass('active')) return + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT + * ==================== */ + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + /* CAROUSEL DATA-API + * ================= */ + + $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = $.extend({}, $target.data(), $this.data()) + , slideIndex + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('carousel').pause().to(slideIndex).cycle() + } + + e.preventDefault() + }) + +}(window.jQuery); \ No newline at end of file diff --git a/include/thirdparty/Bootstrap/js/bootstrap-collapse.js b/include/thirdparty/Bootstrap/js/bootstrap-collapse.js new file mode 100644 index 0000000..2bede4a --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-collapse.js @@ -0,0 +1,167 @@ +/* ============================================================= + * bootstrap-collapse.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning || this.$element.hasClass('in')) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning || !this.$element.hasClass('in')) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSE PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT + * ==================== */ + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + /* COLLAPSE DATA-API + * ================= */ + + $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + +}(window.jQuery); \ No newline at end of file diff --git a/include/thirdparty/Bootstrap/js/bootstrap-dropdown.js b/include/thirdparty/Bootstrap/js/bootstrap-dropdown.js new file mode 100644 index 0000000..a1d5151 --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-dropdown.js @@ -0,0 +1,165 @@ +/* ============================================================ + * bootstrap-dropdown.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + $parent.toggleClass('open') + } + + $this.focus() + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + $(toggle).each(function () { + getParent($(this)).removeClass('open') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = selector && $(selector) + + if (!$parent || !$parent.length) $parent = $this.parent() + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* DROPDOWN NO CONFLICT + * ==================== */ + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(document) + .on('click.dropdown.data-api', clearMenus) + .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.dropdown-menu', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); diff --git a/include/thirdparty/Bootstrap/js/bootstrap-modal.js b/include/thirdparty/Bootstrap/js/bootstrap-modal.js new file mode 100644 index 0000000..12abe06 --- /dev/null +++ b/include/thirdparty/Bootstrap/js/bootstrap-modal.js @@ -0,0 +1,247 @@ +/* ========================================================= + * bootstrap-modal.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element.show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : + that.$element.focus().trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.removeBackdrop() + that.$element.trigger('hidden') + }) + } + + , removeBackdrop: function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file diff --git a/include/thirdparty/Bootstrap3/js/button.js b/include/thirdparty/Bootstrap3/js/button.js new file mode 100644 index 0000000..0f36e41 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/button.js @@ -0,0 +1,120 @@ +/* ======================================================================== + * Bootstrap: button.js v3.3.6 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.VERSION = '3.3.6' + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state += 'Text' + + if (data.resetText == null) $el.data('resetText', $el[val]()) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + $el[val](data[state] == null ? this.options[state] : data[state]) + + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked')) changed = false + $parent.find('.active').removeClass('active') + this.$element.addClass('active') + } else if ($input.prop('type') == 'checkbox') { + if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false + this.$element.toggleClass('active') + } + $input.prop('checked', this.$element.hasClass('active')) + if (changed) $input.trigger('change') + } else { + this.$element.attr('aria-pressed', !this.$element.hasClass('active')) + this.$element.toggleClass('active') + } + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + var old = $.fn.button + + $.fn.button = Plugin + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document) + .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + Plugin.call($btn, 'toggle') + if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() + }) + .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { + $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) + }) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/carousel.js b/include/thirdparty/Bootstrap3/js/carousel.js new file mode 100644 index 0000000..6cdbc79 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/carousel.js @@ -0,0 +1,237 @@ +/* ======================================================================== + * Bootstrap: carousel.js v3.3.6 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = null + this.sliding = null + this.interval = null + this.$active = null + this.$items = null + + this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) + + this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element + .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) + .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + } + + Carousel.VERSION = '3.3.6' + + Carousel.TRANSITION_DURATION = 600 + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true, + keyboard: true + } + + Carousel.prototype.keydown = function (e) { + if (/input|textarea/i.test(e.target.tagName)) return + switch (e.which) { + case 37: this.prev(); break + case 39: this.next(); break + default: return + } + + e.preventDefault() + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getItemIndex = function (item) { + this.$items = item.parent().children('.item') + return this.$items.index(item || this.$active) + } + + Carousel.prototype.getItemForDirection = function (direction, active) { + var activeIndex = this.getItemIndex(active) + var willWrap = (direction == 'prev' && activeIndex === 0) + || (direction == 'next' && activeIndex == (this.$items.length - 1)) + if (willWrap && !this.options.wrap) return active + var delta = direction == 'prev' ? -1 : 1 + var itemIndex = (activeIndex + delta) % this.$items.length + return this.$items.eq(itemIndex) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || this.getItemForDirection(type, $active) + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var that = this + + if ($next.hasClass('active')) return (this.sliding = false) + + var relatedTarget = $next[0] + var slideEvent = $.Event('slide.bs.carousel', { + relatedTarget: relatedTarget, + direction: direction + }) + this.$element.trigger(slideEvent) + if (slideEvent.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) + $nextIndicator && $nextIndicator.addClass('active') + } + + var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one('bsTransitionEnd', function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { + that.$element.trigger(slidEvent) + }, 0) + }) + .emulateTransitionEnd(Carousel.TRANSITION_DURATION) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger(slidEvent) + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + var old = $.fn.carousel + + $.fn.carousel = Plugin + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + var clickHandler = function (e) { + var href + var $this = $(this) + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + Plugin.call($target, options) + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + } + + $(document) + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Plugin.call($carousel, $carousel.data()) + }) + }) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/collapse.js b/include/thirdparty/Bootstrap3/js/collapse.js new file mode 100644 index 0000000..9e26465 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/collapse.js @@ -0,0 +1,211 @@ +/* ======================================================================== + * Bootstrap: collapse.js v3.3.6 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + + '[data-toggle="collapse"][data-target="#' + element.id + '"]') + this.transitioning = null + + if (this.options.parent) { + this.$parent = this.getParent() + } else { + this.addAriaAndCollapsedClass(this.$element, this.$trigger) + } + + if (this.options.toggle) this.toggle() + } + + Collapse.VERSION = '3.3.6' + + Collapse.TRANSITION_DURATION = 350 + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var activesData + var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') + + if (actives && actives.length) { + activesData = actives.data('bs.collapse') + if (activesData && activesData.transitioning) return + } + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + if (actives && actives.length) { + Plugin.call(actives, 'hide') + activesData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing')[dimension](0) + .attr('aria-expanded', true) + + this.$trigger + .removeClass('collapsed') + .attr('aria-expanded', true) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in')[dimension]('') + this.transitioning = 0 + this.$element + .trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element[dimension](this.$element[dimension]())[0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse in') + .attr('aria-expanded', false) + + this.$trigger + .addClass('collapsed') + .attr('aria-expanded', false) + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .removeClass('collapsing') + .addClass('collapse') + .trigger('hidden.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + Collapse.prototype.getParent = function () { + return $(this.options.parent) + .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') + .each($.proxy(function (i, element) { + var $element = $(element) + this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) + }, this)) + .end() + } + + Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { + var isOpen = $element.hasClass('in') + + $element.attr('aria-expanded', isOpen) + $trigger + .toggleClass('collapsed', !isOpen) + .attr('aria-expanded', isOpen) + } + + function getTargetFromTrigger($trigger) { + var href + var target = $trigger.attr('data-target') + || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + + return $(target) + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.collapse + + $.fn.collapse = Plugin + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { + var $this = $(this) + + if (!$this.attr('data-target')) e.preventDefault() + + var $target = getTargetFromTrigger($this) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + + Plugin.call($target, option) + }) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/dropdown.js b/include/thirdparty/Bootstrap3/js/dropdown.js new file mode 100644 index 0000000..df6be86 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/dropdown.js @@ -0,0 +1,165 @@ +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.6 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.6' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/modal.js b/include/thirdparty/Bootstrap3/js/modal.js new file mode 100644 index 0000000..5049ccc --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/modal.js @@ -0,0 +1,337 @@ +/* ======================================================================== + * Bootstrap: modal.js v3.3.6 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$body = $(document.body) + this.$element = $(element) + this.$dialog = this.$element.find('.modal-dialog') + this.$backdrop = null + this.isShown = null + this.originalBodyPad = null + this.scrollbarWidth = 0 + this.ignoreBackdropClick = false + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } + } + + Modal.VERSION = '3.3.6' + + Modal.TRANSITION_DURATION = 300 + Modal.BACKDROP_TRANSITION_DURATION = 150 + + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this.isShown ? this.hide() : this.show(_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.checkScrollbar() + this.setScrollbar() + this.$body.addClass('modal-open') + + this.escape() + this.resize() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.$dialog.on('mousedown.dismiss.bs.modal', function () { + that.$element.one('mouseup.dismiss.bs.modal', function (e) { + if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true + }) + }) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(that.$body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + that.adjustDialog() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$dialog // wait for modal to slide in + .one('bsTransitionEnd', function () { + that.$element.trigger('focus').trigger(e) + }) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + that.$element.trigger('focus').trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + this.resize() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .off('click.dismiss.bs.modal') + .off('mouseup.dismiss.bs.modal') + + this.$dialog.off('mousedown.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one('bsTransitionEnd', $.proxy(this.hideModal, this)) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.trigger('focus') + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keydown.dismiss.bs.modal') + } + } + + Modal.prototype.resize = function () { + if (this.isShown) { + $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) + } else { + $(window).off('resize.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.$body.removeClass('modal-open') + that.resetAdjustments() + that.resetScrollbar() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(document.createElement('div')) + .addClass('modal-backdrop ' + animate) + .appendTo(this.$body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (this.ignoreBackdropClick) { + this.ignoreBackdropClick = false + return + } + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus() + : this.hide() + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one('bsTransitionEnd', callback) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + var callbackRemove = function () { + that.removeBackdrop() + callback && callback() + } + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one('bsTransitionEnd', callbackRemove) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callbackRemove() + + } else if (callback) { + callback() + } + } + + // these following methods are used to handle overflowing modals + + Modal.prototype.handleUpdate = function () { + this.adjustDialog() + } + + Modal.prototype.adjustDialog = function () { + var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight + + this.$element.css({ + paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', + paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' + }) + } + + Modal.prototype.resetAdjustments = function () { + this.$element.css({ + paddingLeft: '', + paddingRight: '' + }) + } + + Modal.prototype.checkScrollbar = function () { + var fullWindowWidth = window.innerWidth + if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 + var documentElementRect = document.documentElement.getBoundingClientRect() + fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) + } + this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth + this.scrollbarWidth = this.measureScrollbar() + } + + Modal.prototype.setScrollbar = function () { + var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) + this.originalBodyPad = document.body.style.paddingRight || '' + if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) + } + + Modal.prototype.resetScrollbar = function () { + this.$body.css('padding-right', this.originalBodyPad) + } + + Modal.prototype.measureScrollbar = function () { // thx walsh + var scrollDiv = document.createElement('div') + scrollDiv.className = 'modal-scrollbar-measure' + this.$body.append(scrollDiv) + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + this.$body[0].removeChild(scrollDiv) + return scrollbarWidth + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + function Plugin(option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + var old = $.fn.modal + + $.fn.modal = Plugin + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target.one('show.bs.modal', function (showEvent) { + if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown + $target.one('hidden.bs.modal', function () { + $this.is(':visible') && $this.trigger('focus') + }) + }) + Plugin.call($target, option, this) + }) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/popover.js b/include/thirdparty/Bootstrap3/js/popover.js new file mode 100644 index 0000000..f2362e0 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/popover.js @@ -0,0 +1,108 @@ +/* ======================================================================== + * Bootstrap: popover.js v3.3.6 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') + + Popover.VERSION = '3.3.6' + + Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right', + trigger: 'click', + content: '', + template: '' + }) + + + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.arrow')) + } + + + // POPOVER PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.popover + + $.fn.popover = Plugin + $.fn.popover.Constructor = Popover + + + // POPOVER NO CONFLICT + // =================== + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/scrollspy.js b/include/thirdparty/Bootstrap3/js/scrollspy.js new file mode 100644 index 0000000..5970b06 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/scrollspy.js @@ -0,0 +1,172 @@ +/* ======================================================================== + * Bootstrap: scrollspy.js v3.3.6 + * http://getbootstrap.com/javascript/#scrollspy + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // SCROLLSPY CLASS DEFINITION + // ========================== + + function ScrollSpy(element, options) { + this.$body = $(document.body) + this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target || '') + ' .nav li > a' + this.offsets = [] + this.targets = [] + this.activeTarget = null + this.scrollHeight = 0 + + this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) + this.refresh() + this.process() + } + + ScrollSpy.VERSION = '3.3.6' + + ScrollSpy.DEFAULTS = { + offset: 10 + } + + ScrollSpy.prototype.getScrollHeight = function () { + return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) + } + + ScrollSpy.prototype.refresh = function () { + var that = this + var offsetMethod = 'offset' + var offsetBase = 0 + + this.offsets = [] + this.targets = [] + this.scrollHeight = this.getScrollHeight() + + if (!$.isWindow(this.$scrollElement[0])) { + offsetMethod = 'position' + offsetBase = this.$scrollElement.scrollTop() + } + + this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#./.test(href) && $(href) + + return ($href + && $href.length + && $href.is(':visible') + && [[$href[offsetMethod]().top + offsetBase, href]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + that.offsets.push(this[0]) + that.targets.push(this[1]) + }) + } + + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.getScrollHeight() + var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i + + if (this.scrollHeight != scrollHeight) { + this.refresh() + } + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) + } + + if (activeTarget && scrollTop < offsets[0]) { + this.activeTarget = null + return this.clear() + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) + && this.activate(targets[i]) + } + } + + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target + + this.clear() + + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + var active = $(selector) + .parents('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + + active.trigger('activate.bs.scrollspy') + } + + ScrollSpy.prototype.clear = function () { + $(this.selector) + .parentsUntil(this.options.target, '.active') + .removeClass('active') + } + + + // SCROLLSPY PLUGIN DEFINITION + // =========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.scrollspy + + $.fn.scrollspy = Plugin + $.fn.scrollspy.Constructor = ScrollSpy + + + // SCROLLSPY NO CONFLICT + // ===================== + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + // SCROLLSPY DATA-API + // ================== + + $(window).on('load.bs.scrollspy.data-api', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + Plugin.call($spy, $spy.data()) + }) + }) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/tab.js b/include/thirdparty/Bootstrap3/js/tab.js new file mode 100644 index 0000000..7d533e8 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/tab.js @@ -0,0 +1,155 @@ +/* ======================================================================== + * Bootstrap: tab.js v3.3.6 + * http://getbootstrap.com/javascript/#tabs + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TAB CLASS DEFINITION + // ==================== + + var Tab = function (element) { + // jscs:disable requireDollarBeforejQueryAssignment + this.element = $(element) + // jscs:enable requireDollarBeforejQueryAssignment + } + + Tab.VERSION = '3.3.6' + + Tab.TRANSITION_DURATION = 150 + + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.data('target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + if ($this.parent('li').hasClass('active')) return + + var $previous = $ul.find('.active:last a') + var hideEvent = $.Event('hide.bs.tab', { + relatedTarget: $this[0] + }) + var showEvent = $.Event('show.bs.tab', { + relatedTarget: $previous[0] + }) + + $previous.trigger(hideEvent) + $this.trigger(showEvent) + + if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return + + var $target = $(selector) + + this.activate($this.closest('li'), $ul) + this.activate($target, $target.parent(), function () { + $previous.trigger({ + type: 'hidden.bs.tab', + relatedTarget: $this[0] + }) + $this.trigger({ + type: 'shown.bs.tab', + relatedTarget: $previous[0] + }) + }) + } + + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', false) + + element + .addClass('active') + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if (element.parent('.dropdown-menu').length) { + element + .closest('li.dropdown') + .addClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + } + + callback && callback() + } + + $active.length && transition ? + $active + .one('bsTransitionEnd', next) + .emulateTransitionEnd(Tab.TRANSITION_DURATION) : + next() + + $active.removeClass('in') + } + + + // TAB PLUGIN DEFINITION + // ===================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tab + + $.fn.tab = Plugin + $.fn.tab.Constructor = Tab + + + // TAB NO CONFLICT + // =============== + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + // TAB DATA-API + // ============ + + var clickHandler = function (e) { + e.preventDefault() + Plugin.call($(this), 'show') + } + + $(document) + .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) + .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/tooltip.js b/include/thirdparty/Bootstrap3/js/tooltip.js new file mode 100644 index 0000000..7094b34 --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/tooltip.js @@ -0,0 +1,514 @@ +/* ======================================================================== + * Bootstrap: tooltip.js v3.3.6 + * http://getbootstrap.com/javascript/#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = null + this.options = null + this.enabled = null + this.timeout = null + this.hoverState = null + this.$element = null + this.inState = null + + this.init('tooltip', element, options) + } + + Tooltip.VERSION = '3.3.6' + + Tooltip.TRANSITION_DURATION = 150 + + Tooltip.DEFAULTS = { + animation: true, + placement: 'top', + selector: false, + template: '', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + container: false, + viewport: { + selector: 'body', + padding: 0 + } + } + + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) + this.inState = { click: false, hover: false, focus: false } + + if (this.$element[0] instanceof document.constructor && !this.options.selector) { + throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') + } + + var triggers = this.options.trigger.split(' ') + + for (var i = triggers.length; i--;) { + var trigger = triggers[i] + + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay, + hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true + } + + if (self.tip().hasClass('in') || self.hoverState == 'in') { + self.hoverState = 'in' + return + } + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.isInStateTrue = function () { + for (var key in this.inState) { + if (this.inState[key]) return true + } + + return false + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false + } + + if (self.isInStateTrue()) return + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.' + this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) + if (e.isDefaultPrevented() || !inDom) return + var that = this + + var $tip = this.tip() + + var tipId = this.getUID(this.type) + + this.setContent() + $tip.attr('id', tipId) + this.$element.attr('aria-describedby', tipId) + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + .addClass(placement) + .data('bs.' + this.type, this) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + this.$element.trigger('inserted.bs.' + this.type) + + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var orgPlacement = placement + var viewportDim = this.getPosition(this.$viewport) + + placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : + placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : + placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) + + this.applyPlacement(calculatedOffset, placement) + + var complete = function () { + var prevHoverState = that.hoverState + that.$element.trigger('shown.bs.' + that.type) + that.hoverState = null + + if (prevHoverState == 'out') that.leave(that) + } + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + } + } + + Tooltip.prototype.applyPlacement = function (offset, placement) { + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top += marginTop + offset.left += marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset($tip[0], $.extend({ + using: function (props) { + $tip.css({ + top: Math.round(props.top), + left: Math.round(props.left) + }) + } + }, offset), 0) + + $tip.addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + offset.top = offset.top + height - actualHeight + } + + var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) + + if (delta.left) offset.left += delta.left + else offset.top += delta.top + + var isVertical = /top|bottom/.test(placement) + var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight + var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' + + $tip.offset(offset) + this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) + } + + Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { + this.arrow() + .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') + .css(isVertical ? 'top' : 'left', '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function (callback) { + var that = this + var $tip = $(this.$tip) + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + that.$element + .removeAttr('aria-describedby') + .trigger('hidden.bs.' + that.type) + callback && callback() + } + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + $.support.transition && $tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + + this.hoverState = null + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function ($element) { + $element = $element || this.$element + + var el = $element[0] + var isBody = el.tagName == 'BODY' + + var elRect = el.getBoundingClientRect() + if (elRect.width == null) { + // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 + elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) + } + var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() + var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } + var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null + + return $.extend({}, elRect, scroll, outerDims, elOffset) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + + } + + Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + if (!this.$viewport) return delta + + var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 + var viewportDimensions = this.getPosition(this.$viewport) + + if (/right|left/.test(placement)) { + var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight + if (topEdgeOffset < viewportDimensions.top) { // top overflow + delta.top = viewportDimensions.top - topEdgeOffset + } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow + delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset + } + } else { + var leftEdgeOffset = pos.left - viewportPadding + var rightEdgeOffset = pos.left + viewportPadding + actualWidth + if (leftEdgeOffset < viewportDimensions.left) { // left overflow + delta.left = viewportDimensions.left - leftEdgeOffset + } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow + delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset + } + } + + return delta + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.getUID = function (prefix) { + do prefix += ~~(Math.random() * 1000000) + while (document.getElementById(prefix)) + return prefix + } + + Tooltip.prototype.tip = function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + if (this.$tip.length != 1) { + throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') + } + } + return this.$tip + } + + Tooltip.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = this + if (e) { + self = $(e.currentTarget).data('bs.' + this.type) + if (!self) { + self = new this.constructor(e.currentTarget, this.getDelegateOptions()) + $(e.currentTarget).data('bs.' + this.type, self) + } + } + + if (e) { + self.inState.click = !self.inState.click + if (self.isInStateTrue()) self.enter(self) + else self.leave(self) + } else { + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + } + + Tooltip.prototype.destroy = function () { + var that = this + clearTimeout(this.timeout) + this.hide(function () { + that.$element.off('.' + that.type).removeData('bs.' + that.type) + if (that.$tip) { + that.$tip.detach() + } + that.$tip = null + that.$arrow = null + that.$viewport = null + }) + } + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tooltip + + $.fn.tooltip = Plugin + $.fn.tooltip.Constructor = Tooltip + + + // TOOLTIP NO CONFLICT + // =================== + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/js/transition.js b/include/thirdparty/Bootstrap3/js/transition.js new file mode 100644 index 0000000..fae36ed --- /dev/null +++ b/include/thirdparty/Bootstrap3/js/transition.js @@ -0,0 +1,59 @@ +/* ======================================================================== + * Bootstrap: transition.js v3.3.6 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); diff --git a/include/thirdparty/Bootstrap3/less/alerts.less b/include/thirdparty/Bootstrap3/less/alerts.less new file mode 100644 index 0000000..c4199db --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/alerts.less @@ -0,0 +1,73 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: @alert-padding; + margin-bottom: @line-height-computed; + border: 1px solid transparent; + border-radius: @alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing @headings-color + color: inherit; + } + + // Provide class for links that match alerts + .alert-link { + font-weight: @alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + + > p + p { + margin-top: 5px; + } +} + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. +.alert-dismissible { + padding-right: (@alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); +} + +.alert-info { + .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); +} + +.alert-warning { + .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); +} + +.alert-danger { + .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); +} diff --git a/include/thirdparty/Bootstrap3/less/badges.less b/include/thirdparty/Bootstrap3/less/badges.less new file mode 100644 index 0000000..6ee16dc --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/badges.less @@ -0,0 +1,66 @@ +// +// Badges +// -------------------------------------------------- + + +// Base class +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: @font-size-small; + font-weight: @badge-font-weight; + color: @badge-color; + line-height: @badge-line-height; + vertical-align: middle; + white-space: nowrap; + text-align: center; + background-color: @badge-bg; + border-radius: @badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + + .btn-xs &, + .btn-group-xs > .btn & { + top: 0; + padding: 1px 5px; + } + + // Hover state, but only for links + a& { + &:hover, + &:focus { + color: @badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Account for badges in navs + .list-group-item.active > &, + .nav-pills > .active > a > & { + color: @badge-active-color; + background-color: @badge-active-bg; + } + + .list-group-item > & { + float: right; + } + + .list-group-item > & + & { + margin-right: 5px; + } + + .nav-pills > li > a > & { + margin-left: 3px; + } +} diff --git a/include/thirdparty/Bootstrap3/less/bootstrap.less b/include/thirdparty/Bootstrap3/less/bootstrap.less new file mode 100644 index 0000000..1c04778 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/bootstrap.less @@ -0,0 +1,56 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +// Core variables and mixins +@import "variables.less"; +@import "mixins.less"; + +// Reset and dependencies +@import "normalize.less"; +@import "print.less"; +@import "glyphicons.less"; + +// Core CSS +@import "scaffolding.less"; +@import "type.less"; +@import "code.less"; +@import "grid.less"; +@import "tables.less"; +@import "forms.less"; +@import "buttons.less"; + +// Components +@import "component-animations.less"; +@import "dropdowns.less"; +@import "button-groups.less"; +@import "input-groups.less"; +@import "navs.less"; +@import "navbar.less"; +@import "breadcrumbs.less"; +@import "pagination.less"; +@import "pager.less"; +@import "labels.less"; +@import "badges.less"; +@import "jumbotron.less"; +@import "thumbnails.less"; +@import "alerts.less"; +@import "progress-bars.less"; +@import "media.less"; +@import "list-group.less"; +@import "panels.less"; +@import "responsive-embed.less"; +@import "wells.less"; +@import "close.less"; + +// Components w/ JavaScript +@import "modals.less"; +@import "tooltip.less"; +@import "popovers.less"; +@import "carousel.less"; + +// Utility classes +@import "utilities.less"; +@import "responsive-utilities.less"; diff --git a/include/thirdparty/Bootstrap3/less/bootstrap_all.less b/include/thirdparty/Bootstrap3/less/bootstrap_all.less new file mode 100644 index 0000000..1d89353 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/bootstrap_all.less @@ -0,0 +1 @@ +@import "bootstrap.less"; diff --git a/include/thirdparty/Bootstrap3/less/breadcrumbs.less b/include/thirdparty/Bootstrap3/less/breadcrumbs.less new file mode 100644 index 0000000..cb01d50 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/breadcrumbs.less @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; + margin-bottom: @line-height-computed; + list-style: none; + background-color: @breadcrumb-bg; + border-radius: @border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: @breadcrumb-color; + } + } + + > .active { + color: @breadcrumb-active-color; + } +} diff --git a/include/thirdparty/Bootstrap3/less/button-groups.less b/include/thirdparty/Bootstrap3/less/button-groups.less new file mode 100644 index 0000000..293245a --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/button-groups.less @@ -0,0 +1,244 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + &:extend(.clearfix all); + + .btn, + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + .border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + .border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-right-radius(0); + } +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { &:extend(.btn-xs); } +.btn-group-sm > .btn { &:extend(.btn-sm); } +.btn-group-lg > .btn { &:extend(.btn-lg); } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + .box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: @caret-width-large @caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 @caret-width-large @caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + &:extend(.clearfix all); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + .border-top-radius(@btn-border-radius-base); + .border-bottom-radius(0); + } + &:last-child:not(:first-child) { + .border-top-radius(0); + .border-bottom-radius(@btn-border-radius-base); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-top-radius(0); +} + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } + + > .btn-group .dropdown-menu { + left: auto; + } +} + + +// Checkbox and radio options +// +// In order to support the browser's form validation feedback, powered by the +// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use +// `display: none;` or `visibility: hidden;` as that also hides the popover. +// Simply visually hiding the inputs via `opacity` would leave them clickable in +// certain cases which is prevented by using `clip` and `pointer-events`. +// This way, we ensure a DOM element is visible to position the popover from. +// +// See https://github.com/twbs/bootstrap/pull/12794 and +// https://github.com/twbs/bootstrap/pull/14559 for more information. + +[data-toggle="buttons"] { + > .btn, + > .btn-group > .btn { + input[type="radio"], + input[type="checkbox"] { + position: absolute; + clip: rect(0,0,0,0); + pointer-events: none; + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/buttons.less b/include/thirdparty/Bootstrap3/less/buttons.less new file mode 100644 index 0000000..9cbb8f4 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/buttons.less @@ -0,0 +1,166 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: @btn-font-weight; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base); + .user-select(none); + + &, + &:active, + &.active { + &:focus, + &.focus { + .tab-focus(); + } + } + + &:hover, + &:focus, + &.focus { + color: @btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: @cursor-disabled; + .opacity(.65); + .box-shadow(none); + } + + a& { + &.disabled, + fieldset[disabled] & { + pointer-events: none; // Future-proof disabling of clicks on `` elements + } + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); +} +.btn-primary { + .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); +} +// Success appears as green +.btn-success { + .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); +} +// Info appears as blue-green +.btn-info { + .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); +} +// Warning appears as orange +.btn-warning { + .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: @link-color; + font-weight: normal; + border-radius: 0; + + &, + &:active, + &.active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + .box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: @link-hover-decoration; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); +} +.btn-xs { + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/include/thirdparty/Bootstrap3/less/carousel.less b/include/thirdparty/Bootstrap3/less/carousel.less new file mode 100644 index 0000000..252011e --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/carousel.less @@ -0,0 +1,270 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + .transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + &:extend(.img-responsive); + line-height: 1; + } + + // WebKit CSS3 transforms for supported devices + @media all and (transform-3d), (-webkit-transform-3d) { + .transition-transform(~'0.6s ease-in-out'); + .backface-visibility(~'hidden'); + .perspective(1000px); + + &.next, + &.active.right { + .translate3d(100%, 0, 0); + left: 0; + } + &.prev, + &.active.left { + .translate3d(-100%, 0, 0); + left: 0; + } + &.next.left, + &.prev.right, + &.active { + .translate3d(0, 0, 0); + left: 0; + } + } + } + + > .active, + > .next, + > .prev { + display: block; + } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: @carousel-control-width; + .opacity(@carousel-control-opacity); + font-size: @carousel-control-font-size; + color: @carousel-control-color; + text-align: center; + text-shadow: @carousel-text-shadow; + background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: 0; + color: @carousel-control-color; + text-decoration: none; + .opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + margin-top: -10px; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + line-height: 1; + font-family: serif; + } + + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid @carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: @carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: @carousel-caption-color; + text-align: center; + text-shadow: @carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: @screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: (@carousel-control-font-size * 1.5); + height: (@carousel-control-font-size * 1.5); + margin-top: (@carousel-control-font-size / -2); + font-size: (@carousel-control-font-size * 1.5); + } + .glyphicon-chevron-left, + .icon-prev { + margin-left: (@carousel-control-font-size / -2); + } + .glyphicon-chevron-right, + .icon-next { + margin-right: (@carousel-control-font-size / -2); + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/include/thirdparty/Bootstrap3/less/close.less b/include/thirdparty/Bootstrap3/less/close.less new file mode 100644 index 0000000..6d5bfe0 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/close.less @@ -0,0 +1,34 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: (@font-size-base * 1.5); + font-weight: @close-font-weight; + line-height: 1; + color: @close-color; + text-shadow: @close-text-shadow; + .opacity(.2); + + &:hover, + &:focus { + color: @close-color; + text-decoration: none; + cursor: pointer; + .opacity(.5); + } + + // Additional properties for button version + // iOS requires the button element instead of an anchor tag. + // If you want the anchor version, it requires `href="#"`. + // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + button& { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + } +} diff --git a/include/thirdparty/Bootstrap3/less/code.less b/include/thirdparty/Bootstrap3/less/code.less new file mode 100644 index 0000000..a08b4d4 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/code.less @@ -0,0 +1,69 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: @font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: @code-color; + background-color: @code-bg; + border-radius: @border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: @kbd-color; + background-color: @kbd-bg; + border-radius: @border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); + + kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + box-shadow: none; + } +} + +// Blocks of code +pre { + display: block; + padding: ((@line-height-computed - 1) / 2); + margin: 0 0 (@line-height-computed / 2); + font-size: (@font-size-base - 1); // 14px to 13px + line-height: @line-height-base; + word-break: break-all; + word-wrap: break-word; + color: @pre-color; + background-color: @pre-bg; + border: 1px solid @pre-border-color; + border-radius: @border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: @pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/include/thirdparty/Bootstrap3/less/component-animations.less b/include/thirdparty/Bootstrap3/less/component-animations.less new file mode 100644 index 0000000..0bcee91 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/component-animations.less @@ -0,0 +1,33 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. + +.fade { + opacity: 0; + .transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + + &.in { display: block; } + tr&.in { display: table-row; } + tbody&.in { display: table-row-group; } +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + .transition-property(~"height, visibility"); + .transition-duration(.35s); + .transition-timing-function(ease); +} diff --git a/include/thirdparty/Bootstrap3/less/dropdowns.less b/include/thirdparty/Bootstrap3/less/dropdowns.less new file mode 100644 index 0000000..f6876c1 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/dropdowns.less @@ -0,0 +1,216 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: @caret-width-base dashed; + border-top: @caret-width-base solid ~"\9"; // IE8 + border-right: @caret-width-base solid transparent; + border-left: @caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropup, +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: @zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: @font-size-base; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + background-color: @dropdown-bg; + border: 1px solid @dropdown-fallback-border; // IE8 fallback + border: 1px solid @dropdown-border; + border-radius: @border-radius-base; + .box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + .nav-divider(@dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: @line-height-base; + color: @dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: @dropdown-link-hover-color; + background-color: @dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: @dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: @dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: @dropdown-link-disabled-color; + } + + // Nuke hover/focus effects + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + .reset-filter(); + cursor: @cursor-disabled; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: @font-size-small; + line-height: @line-height-base; + color: @dropdown-header-color; + white-space: nowrap; // as with > li > a +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: (@zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: @caret-width-base dashed; + border-bottom: @caret-width-base solid ~"\9"; // IE8 + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: @grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + .dropdown-menu-right(); + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + .dropdown-menu-left(); + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/forms.less b/include/thirdparty/Bootstrap3/less/forms.less new file mode 100644 index 0000000..e8b071a --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/forms.less @@ -0,0 +1,613 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a `min-width: min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: @line-height-computed; + font-size: (@font-size-base * 1.5); + line-height: inherit; + color: @legend-color; + border: 0; + border-bottom: 1px solid @legend-border-color; +} + +label { + display: inline-block; + max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) + margin-bottom: 5px; + font-weight: bold; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (* isn't specific enough) +input[type="search"] { + .box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; // IE8-9 + line-height: normal; +} + +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + .tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: (@padding-base-vertical + 1); + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +// select +// textarea +// input[type="text"] +// input[type="password"] +// input[type="datetime"] +// input[type="datetime-local"] +// input[type="date"] +// input[type="month"] +// input[type="time"] +// input[type="week"] +// input[type="number"] +// input[type="email"] +// input[type="url"] +// input[type="search"] +// input[type="tel"] +// input[type="color"] + +.form-control { + display: block; + width: 100%; + height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid @input-border; + border-radius: @input-border-radius; // Note: This has no effect on s in CSS. + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); + + // Customize the `:focus` state to imitate native WebKit styles. + .form-control-focus(); + + // Placeholder + .placeholder(); + + // Unstyle the caret on `` +// element gets special love because it's special, and that's a fact! +.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + height: @input-height; + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + border-radius: @border-radius; + + select& { + height: @input-height; + line-height: @input-height; + } + + textarea&, + select[multiple]& { + height: auto; + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/gradients.less b/include/thirdparty/Bootstrap3/less/mixins/gradients.less new file mode 100644 index 0000000..0b88a89 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/gradients.less @@ -0,0 +1,59 @@ +// Gradients + +#gradient { + + // Horizontal gradient, from left to right + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12 + background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down + } + + // Vertical gradient, from top to bottom + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12 + background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down + } + + .directional(@start-color: #555; @end-color: #333; @deg: 45deg) { + background-repeat: repeat-x; + background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12 + background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + } + .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); + background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .radial(@inner-color: #555; @outer-color: #333) { + background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color); + background-image: radial-gradient(circle, @inner-color, @outer-color); + background-repeat: no-repeat; + } + .striped(@color: rgba(255,255,255,.15); @angle: 45deg) { + background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/grid-framework.less b/include/thirdparty/Bootstrap3/less/mixins/grid-framework.less new file mode 100644 index 0000000..8c23eed --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/grid-framework.less @@ -0,0 +1,91 @@ +// Framework grid generation +// +// Used only by Bootstrap to generate the correct number of grid classes given +// any value of `@grid-columns`. + +.make-grid-columns() { + // Common styles for all sizes of grid columns, widths 1-12 + .col(@index) { // initial + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + position: relative; + // Prevent columns from collapsing when empty + min-height: 1px; + // Inner gutter via padding + padding-left: ceil((@grid-gutter-width / 2)); + padding-right: floor((@grid-gutter-width / 2)); + } + } + .col(1); // kickstart it +} + +.float-grid-columns(@class) { + .col(@index) { // initial + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + float: left; + } + } + .col(1); // kickstart it +} + +.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { + .col-@{class}-@{index} { + width: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) { + .col-@{class}-push-@{index} { + left: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) { + .col-@{class}-push-0 { + left: auto; + } +} +.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) { + .col-@{class}-pull-@{index} { + right: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) { + .col-@{class}-pull-0 { + right: auto; + } +} +.calc-grid-column(@index, @class, @type) when (@type = offset) { + .col-@{class}-offset-@{index} { + margin-left: percentage((@index / @grid-columns)); + } +} + +// Basic looping in LESS +.loop-grid-columns(@index, @class, @type) when (@index >= 0) { + .calc-grid-column(@index, @class, @type); + // next iteration + .loop-grid-columns((@index - 1), @class, @type); +} + +// Create grid for specific class +.make-grid(@class) { + .float-grid-columns(@class); + .loop-grid-columns(@grid-columns, @class, width); + .loop-grid-columns(@grid-columns, @class, pull); + .loop-grid-columns(@grid-columns, @class, push); + .loop-grid-columns(@grid-columns, @class, offset); +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/grid.less b/include/thirdparty/Bootstrap3/less/mixins/grid.less new file mode 100644 index 0000000..df496d0 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/grid.less @@ -0,0 +1,122 @@ +// Grid system +// +// Generate semantic grid columns with these mixins. + +// Centered container element +.container-fixed(@gutter: @grid-gutter-width) { + margin-right: auto; + margin-left: auto; + padding-left: floor((@gutter / 2)); + padding-right: ceil((@gutter / 2)); + &:extend(.clearfix all); +} + +// Creates a wrapper for a series of columns +.make-row(@gutter: @grid-gutter-width) { + margin-left: ceil((@gutter / -2)); + margin-right: floor((@gutter / -2)); + &:extend(.clearfix all); +} + +// Generate the extra small columns +.make-xs-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + float: left; + width: percentage((@columns / @grid-columns)); + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); +} +.make-xs-column-offset(@columns) { + margin-left: percentage((@columns / @grid-columns)); +} +.make-xs-column-push(@columns) { + left: percentage((@columns / @grid-columns)); +} +.make-xs-column-pull(@columns) { + right: percentage((@columns / @grid-columns)); +} + +// Generate the small columns +.make-sm-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-sm-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-offset(@columns) { + @media (min-width: @screen-sm-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-push(@columns) { + @media (min-width: @screen-sm-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-pull(@columns) { + @media (min-width: @screen-sm-min) { + right: percentage((@columns / @grid-columns)); + } +} + +// Generate the medium columns +.make-md-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-md-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-md-column-offset(@columns) { + @media (min-width: @screen-md-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-push(@columns) { + @media (min-width: @screen-md-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-pull(@columns) { + @media (min-width: @screen-md-min) { + right: percentage((@columns / @grid-columns)); + } +} + +// Generate the large columns +.make-lg-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-lg-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-offset(@columns) { + @media (min-width: @screen-lg-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-push(@columns) { + @media (min-width: @screen-lg-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-pull(@columns) { + @media (min-width: @screen-lg-min) { + right: percentage((@columns / @grid-columns)); + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/hide-text.less b/include/thirdparty/Bootstrap3/less/mixins/hide-text.less new file mode 100644 index 0000000..2bb84a3 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/hide-text.less @@ -0,0 +1,21 @@ +// CSS image replacement +// +// Heads up! v3 launched with only `.hide-text()`, but per our pattern for +// mixins being reused as classes with the same name, this doesn't hold up. As +// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. +// +// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 + +// Deprecated as of v3.0.1 (has been removed in v4) +.hide-text() { + font: ~"0/0" a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +// New mixin to use as of v3.0.1 +.text-hide() { + .hide-text(); +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/image.less b/include/thirdparty/Bootstrap3/less/mixins/image.less new file mode 100644 index 0000000..f233cb3 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/image.less @@ -0,0 +1,33 @@ +// Image Mixins +// - Responsive image +// - Retina image + + +// Responsive image +// +// Keep images from scaling beyond the width of their parents. +.img-responsive(@display: block) { + display: @display; + max-width: 100%; // Part 1: Set a maximum relative to the parent + height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching +} + + +// Retina image +// +// Short retina mixin for setting background-image and -size. Note that the +// spelling of `min--moz-device-pixel-ratio` is intentional. +.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { + background-image: url("@{file-1x}"); + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + background-image: url("@{file-2x}"); + background-size: @width-1x @height-1x; + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/labels.less b/include/thirdparty/Bootstrap3/less/mixins/labels.less new file mode 100644 index 0000000..9f7a67e --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/labels.less @@ -0,0 +1,12 @@ +// Labels + +.label-variant(@color) { + background-color: @color; + + &[href] { + &:hover, + &:focus { + background-color: darken(@color, 10%); + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/list-group.less b/include/thirdparty/Bootstrap3/less/mixins/list-group.less new file mode 100644 index 0000000..03aa190 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/list-group.less @@ -0,0 +1,30 @@ +// List Groups + +.list-group-item-variant(@state; @background; @color) { + .list-group-item-@{state} { + color: @color; + background-color: @background; + + a&, + button& { + color: @color; + + .list-group-item-heading { + color: inherit; + } + + &:hover, + &:focus { + color: @color; + background-color: darken(@background, 5%); + } + &.active, + &.active:hover, + &.active:focus { + color: #fff; + background-color: @color; + border-color: @color; + } + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/nav-divider.less b/include/thirdparty/Bootstrap3/less/mixins/nav-divider.less new file mode 100644 index 0000000..feb1e9e --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/nav-divider.less @@ -0,0 +1,10 @@ +// Horizontal dividers +// +// Dividers (basically an hr) within dropdowns and nav lists + +.nav-divider(@color: #e5e5e5) { + height: 1px; + margin: ((@line-height-computed / 2) - 1) 0; + overflow: hidden; + background-color: @color; +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/nav-vertical-align.less b/include/thirdparty/Bootstrap3/less/mixins/nav-vertical-align.less new file mode 100644 index 0000000..d458c78 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/nav-vertical-align.less @@ -0,0 +1,9 @@ +// Navbar vertical align +// +// Vertically center elements in the navbar. +// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. + +.navbar-vertical-align(@element-height) { + margin-top: ((@navbar-height - @element-height) / 2); + margin-bottom: ((@navbar-height - @element-height) / 2); +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/opacity.less b/include/thirdparty/Bootstrap3/less/mixins/opacity.less new file mode 100644 index 0000000..33ed25c --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/opacity.less @@ -0,0 +1,8 @@ +// Opacity + +.opacity(@opacity) { + opacity: @opacity; + // IE8 filter + @opacity-ie: (@opacity * 100); + filter: ~"alpha(opacity=@{opacity-ie})"; +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/pagination.less b/include/thirdparty/Bootstrap3/less/mixins/pagination.less new file mode 100644 index 0000000..618804f --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/pagination.less @@ -0,0 +1,24 @@ +// Pagination + +.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + > li { + > a, + > span { + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + } + &:first-child { + > a, + > span { + .border-left-radius(@border-radius); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius); + } + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/panels.less b/include/thirdparty/Bootstrap3/less/mixins/panels.less new file mode 100644 index 0000000..49ee10d --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/panels.less @@ -0,0 +1,24 @@ +// Panels + +.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { + border-color: @border; + + & > .panel-heading { + color: @heading-text-color; + background-color: @heading-bg-color; + border-color: @heading-border; + + + .panel-collapse > .panel-body { + border-top-color: @border; + } + .badge { + color: @heading-bg-color; + background-color: @heading-text-color; + } + } + & > .panel-footer { + + .panel-collapse > .panel-body { + border-bottom-color: @border; + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/progress-bar.less b/include/thirdparty/Bootstrap3/less/mixins/progress-bar.less new file mode 100644 index 0000000..f07996a --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/progress-bar.less @@ -0,0 +1,10 @@ +// Progress bars + +.progress-bar-variant(@color) { + background-color: @color; + + // Deprecated parent class requirement as of v3.2.0 + .progress-striped & { + #gradient > .striped(); + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/reset-filter.less b/include/thirdparty/Bootstrap3/less/mixins/reset-filter.less new file mode 100644 index 0000000..68cdb5e --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/reset-filter.less @@ -0,0 +1,8 @@ +// Reset filters for IE +// +// When you need to remove a gradient background, do not forget to use this to reset +// the IE filter for IE9 and below. + +.reset-filter() { + filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/reset-text.less b/include/thirdparty/Bootstrap3/less/mixins/reset-text.less new file mode 100644 index 0000000..58dd4d1 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/reset-text.less @@ -0,0 +1,18 @@ +.reset-text() { + font-family: @font-family-base; + // We deliberately do NOT reset font-size. + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: @line-height-base; + text-align: left; // Fallback for where `start` is not supported + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/resize.less b/include/thirdparty/Bootstrap3/less/mixins/resize.less new file mode 100644 index 0000000..3acd3af --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/resize.less @@ -0,0 +1,6 @@ +// Resize anything + +.resizable(@direction) { + resize: @direction; // Options: horizontal, vertical, both + overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/responsive-visibility.less b/include/thirdparty/Bootstrap3/less/mixins/responsive-visibility.less new file mode 100644 index 0000000..ecf1e97 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/responsive-visibility.less @@ -0,0 +1,15 @@ +// Responsive utilities + +// +// More easily include all the states for responsive-utilities.less. +.responsive-visibility() { + display: block !important; + table& { display: table !important; } + tr& { display: table-row !important; } + th&, + td& { display: table-cell !important; } +} + +.responsive-invisibility() { + display: none !important; +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/size.less b/include/thirdparty/Bootstrap3/less/mixins/size.less new file mode 100644 index 0000000..a8be650 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/size.less @@ -0,0 +1,10 @@ +// Sizing shortcuts + +.size(@width; @height) { + width: @width; + height: @height; +} + +.square(@size) { + .size(@size; @size); +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/tab-focus.less b/include/thirdparty/Bootstrap3/less/mixins/tab-focus.less new file mode 100644 index 0000000..1f1f05a --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/tab-focus.less @@ -0,0 +1,9 @@ +// WebKit-style focus + +.tab-focus() { + // Default + outline: thin dotted; + // WebKit + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/table-row.less b/include/thirdparty/Bootstrap3/less/mixins/table-row.less new file mode 100644 index 0000000..0f287f1 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/table-row.less @@ -0,0 +1,28 @@ +// Tables + +.table-row-variant(@state; @background) { + // Exact selectors below required to override `.table-striped` and prevent + // inheritance to nested tables. + .table > thead > tr, + .table > tbody > tr, + .table > tfoot > tr { + > td.@{state}, + > th.@{state}, + &.@{state} > td, + &.@{state} > th { + background-color: @background; + } + } + + // Hover states for `.table-hover` + // Note: this is not available for cells or rows within `thead` or `tfoot`. + .table-hover > tbody > tr { + > td.@{state}:hover, + > th.@{state}:hover, + &.@{state}:hover > td, + &:hover > .@{state}, + &.@{state}:hover > th { + background-color: darken(@background, 5%); + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/text-emphasis.less b/include/thirdparty/Bootstrap3/less/mixins/text-emphasis.less new file mode 100644 index 0000000..9e8a77a --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/text-emphasis.less @@ -0,0 +1,9 @@ +// Typography + +.text-emphasis-variant(@color) { + color: @color; + a&:hover, + a&:focus { + color: darken(@color, 10%); + } +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/text-overflow.less b/include/thirdparty/Bootstrap3/less/mixins/text-overflow.less new file mode 100644 index 0000000..c11ad2f --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/text-overflow.less @@ -0,0 +1,8 @@ +// Text overflow +// Requires inline-block or block for proper styling + +.text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/include/thirdparty/Bootstrap3/less/mixins/vendor-prefixes.less b/include/thirdparty/Bootstrap3/less/mixins/vendor-prefixes.less new file mode 100644 index 0000000..2b5e74b --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/mixins/vendor-prefixes.less @@ -0,0 +1,227 @@ +// Vendor Prefixes +// +// All vendor mixins are deprecated as of v3.2.0 due to the introduction of +// Autoprefixer in our Gruntfile. They have been removed in v4. + +// - Animations +// - Backface visibility +// - Box shadow +// - Box sizing +// - Content columns +// - Hyphens +// - Placeholder text +// - Transformations +// - Transitions +// - User Select + + +// Animations +.animation(@animation) { + -webkit-animation: @animation; + -o-animation: @animation; + animation: @animation; +} +.animation-name(@name) { + -webkit-animation-name: @name; + animation-name: @name; +} +.animation-duration(@duration) { + -webkit-animation-duration: @duration; + animation-duration: @duration; +} +.animation-timing-function(@timing-function) { + -webkit-animation-timing-function: @timing-function; + animation-timing-function: @timing-function; +} +.animation-delay(@delay) { + -webkit-animation-delay: @delay; + animation-delay: @delay; +} +.animation-iteration-count(@iteration-count) { + -webkit-animation-iteration-count: @iteration-count; + animation-iteration-count: @iteration-count; +} +.animation-direction(@direction) { + -webkit-animation-direction: @direction; + animation-direction: @direction; +} +.animation-fill-mode(@fill-mode) { + -webkit-animation-fill-mode: @fill-mode; + animation-fill-mode: @fill-mode; +} + +// Backface visibility +// Prevent browsers from flickering when using CSS 3D transforms. +// Default value is `visible`, but can be changed to `hidden` + +.backface-visibility(@visibility) { + -webkit-backface-visibility: @visibility; + -moz-backface-visibility: @visibility; + backface-visibility: @visibility; +} + +// Drop shadows +// +// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's +// supported browsers that have box shadow capabilities now support it. + +.box-shadow(@shadow) { + -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 + box-shadow: @shadow; +} + +// Box sizing +.box-sizing(@boxmodel) { + -webkit-box-sizing: @boxmodel; + -moz-box-sizing: @boxmodel; + box-sizing: @boxmodel; +} + +// CSS3 Content Columns +.content-columns(@column-count; @column-gap: @grid-gutter-width) { + -webkit-column-count: @column-count; + -moz-column-count: @column-count; + column-count: @column-count; + -webkit-column-gap: @column-gap; + -moz-column-gap: @column-gap; + column-gap: @column-gap; +} + +// Optional hyphenation +.hyphens(@mode: auto) { + word-wrap: break-word; + -webkit-hyphens: @mode; + -moz-hyphens: @mode; + -ms-hyphens: @mode; // IE10+ + -o-hyphens: @mode; + hyphens: @mode; +} + +// Placeholder text +.placeholder(@color: @input-color-placeholder) { + // Firefox + &::-moz-placeholder { + color: @color; + opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526 + } + &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ + &::-webkit-input-placeholder { color: @color; } // Safari and Chrome +} + +// Transformations +.scale(@ratio) { + -webkit-transform: scale(@ratio); + -ms-transform: scale(@ratio); // IE9 only + -o-transform: scale(@ratio); + transform: scale(@ratio); +} +.scale(@ratioX; @ratioY) { + -webkit-transform: scale(@ratioX, @ratioY); + -ms-transform: scale(@ratioX, @ratioY); // IE9 only + -o-transform: scale(@ratioX, @ratioY); + transform: scale(@ratioX, @ratioY); +} +.scaleX(@ratio) { + -webkit-transform: scaleX(@ratio); + -ms-transform: scaleX(@ratio); // IE9 only + -o-transform: scaleX(@ratio); + transform: scaleX(@ratio); +} +.scaleY(@ratio) { + -webkit-transform: scaleY(@ratio); + -ms-transform: scaleY(@ratio); // IE9 only + -o-transform: scaleY(@ratio); + transform: scaleY(@ratio); +} +.skew(@x; @y) { + -webkit-transform: skewX(@x) skewY(@y); + -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ + -o-transform: skewX(@x) skewY(@y); + transform: skewX(@x) skewY(@y); +} +.translate(@x; @y) { + -webkit-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); // IE9 only + -o-transform: translate(@x, @y); + transform: translate(@x, @y); +} +.translate3d(@x; @y; @z) { + -webkit-transform: translate3d(@x, @y, @z); + transform: translate3d(@x, @y, @z); +} +.rotate(@degrees) { + -webkit-transform: rotate(@degrees); + -ms-transform: rotate(@degrees); // IE9 only + -o-transform: rotate(@degrees); + transform: rotate(@degrees); +} +.rotateX(@degrees) { + -webkit-transform: rotateX(@degrees); + -ms-transform: rotateX(@degrees); // IE9 only + -o-transform: rotateX(@degrees); + transform: rotateX(@degrees); +} +.rotateY(@degrees) { + -webkit-transform: rotateY(@degrees); + -ms-transform: rotateY(@degrees); // IE9 only + -o-transform: rotateY(@degrees); + transform: rotateY(@degrees); +} +.perspective(@perspective) { + -webkit-perspective: @perspective; + -moz-perspective: @perspective; + perspective: @perspective; +} +.perspective-origin(@perspective) { + -webkit-perspective-origin: @perspective; + -moz-perspective-origin: @perspective; + perspective-origin: @perspective; +} +.transform-origin(@origin) { + -webkit-transform-origin: @origin; + -moz-transform-origin: @origin; + -ms-transform-origin: @origin; // IE9 only + transform-origin: @origin; +} + + +// Transitions + +.transition(@transition) { + -webkit-transition: @transition; + -o-transition: @transition; + transition: @transition; +} +.transition-property(@transition-property) { + -webkit-transition-property: @transition-property; + transition-property: @transition-property; +} +.transition-delay(@transition-delay) { + -webkit-transition-delay: @transition-delay; + transition-delay: @transition-delay; +} +.transition-duration(@transition-duration) { + -webkit-transition-duration: @transition-duration; + transition-duration: @transition-duration; +} +.transition-timing-function(@timing-function) { + -webkit-transition-timing-function: @timing-function; + transition-timing-function: @timing-function; +} +.transition-transform(@transition) { + -webkit-transition: -webkit-transform @transition; + -moz-transition: -moz-transform @transition; + -o-transition: -o-transform @transition; + transition: transform @transition; +} + + +// User select +// For selecting text on the page + +.user-select(@select) { + -webkit-user-select: @select; + -moz-user-select: @select; + -ms-user-select: @select; // IE10+ + user-select: @select; +} diff --git a/include/thirdparty/Bootstrap3/less/modals.less b/include/thirdparty/Bootstrap3/less/modals.less new file mode 100644 index 0000000..767ce36 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/modals.less @@ -0,0 +1,150 @@ +// +// Modals +// -------------------------------------------------- + +// .modal-open - body class for killing the scroll +// .modal - container to scroll within +// .modal-dialog - positioning shell for the actual modal +// .modal-content - actual modal w/ bg and corners and shit + +// Kill the scroll on the body +.modal-open { + overflow: hidden; +} + +// Container that the modal scrolls within +.modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + -webkit-overflow-scrolling: touch; + + // Prevent Chrome on Windows from adding a focus outline. For details, see + // https://github.com/twbs/bootstrap/pull/10951. + outline: 0; + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog { + .translate(0, -25%); + .transition-transform(~"0.3s ease-out"); + } + &.in .modal-dialog { .translate(0, 0) } +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +// Shell div to position the modal with bottom padding +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +// Actual modal +.modal-content { + position: relative; + background-color: @modal-content-bg; + border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc) + border: 1px solid @modal-content-border-color; + border-radius: @border-radius-large; + .box-shadow(0 3px 9px rgba(0,0,0,.5)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: 0; +} + +// Modal background +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal-background; + background-color: @modal-backdrop-bg; + // Fade for backdrop + &.fade { .opacity(0); } + &.in { .opacity(@modal-backdrop-opacity); } +} + +// Modal header +// Top section of the modal w/ title and dismiss +.modal-header { + padding: @modal-title-padding; + border-bottom: 1px solid @modal-header-border-color; + &:extend(.clearfix all); +} +// Close icon +.modal-header .close { + margin-top: -2px; +} + +// Title text within header +.modal-title { + margin: 0; + line-height: @modal-title-line-height; +} + +// Modal body +// Where all modal content resides (sibling of .modal-header and .modal-footer) +.modal-body { + position: relative; + padding: @modal-inner-padding; +} + +// Footer (for actions) +.modal-footer { + padding: @modal-inner-padding; + text-align: right; // right align buttons + border-top: 1px solid @modal-footer-border-color; + &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons + + // Properly space out buttons + .btn + .btn { + margin-left: 5px; + margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs + } + // but override that for button groups + .btn-group .btn + .btn { + margin-left: -1px; + } + // and override it for block buttons as well + .btn-block + .btn-block { + margin-left: 0; + } +} + +// Measure scrollbar width for padding body during modal show/hide +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +// Scale up the modal +@media (min-width: @screen-sm-min) { + // Automatically set modal's width for larger viewports + .modal-dialog { + width: @modal-md; + margin: 30px auto; + } + .modal-content { + .box-shadow(0 5px 15px rgba(0,0,0,.5)); + } + + // Modal sizes + .modal-sm { width: @modal-sm; } +} + +@media (min-width: @screen-md-min) { + .modal-lg { width: @modal-lg; } +} diff --git a/include/thirdparty/Bootstrap3/less/navbar.less b/include/thirdparty/Bootstrap3/less/navbar.less new file mode 100644 index 0000000..6d751bb --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/navbar.less @@ -0,0 +1,660 @@ +// +// Navbars +// -------------------------------------------------- + + +// Wrapper and base class +// +// Provide a static navbar from which we expand to create full-width, fixed, and +// other navbar variations. + +.navbar { + position: relative; + min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) + margin-bottom: @navbar-margin-bottom; + border: 1px solid transparent; + + // Prevent floats from breaking the navbar + &:extend(.clearfix all); + + @media (min-width: @grid-float-breakpoint) { + border-radius: @navbar-border-radius; + } +} + + +// Navbar heading +// +// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy +// styling of responsive aspects. + +.navbar-header { + &:extend(.clearfix all); + + @media (min-width: @grid-float-breakpoint) { + float: left; + } +} + + +// Navbar collapse (body) +// +// Group your navbar content into this for easy collapsing and expanding across +// various device sizes. By default, this content is collapsed when <768px, but +// will expand past that for a horizontal display. +// +// To start (on mobile devices) the navbar links, forms, and buttons are stacked +// vertically and include a `max-height` to overflow in case you have too much +// content for the user's viewport. + +.navbar-collapse { + overflow-x: visible; + padding-right: @navbar-padding-horizontal; + padding-left: @navbar-padding-horizontal; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255,255,255,.1); + &:extend(.clearfix all); + -webkit-overflow-scrolling: touch; + + &.in { + overflow-y: auto; + } + + @media (min-width: @grid-float-breakpoint) { + width: auto; + border-top: 0; + box-shadow: none; + + &.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; // Override default setting + overflow: visible !important; + } + + &.in { + overflow-y: visible; + } + + // Undo the collapse side padding for navbars with containers to ensure + // alignment of right-aligned contents. + .navbar-fixed-top &, + .navbar-static-top &, + .navbar-fixed-bottom & { + padding-left: 0; + padding-right: 0; + } + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + .navbar-collapse { + max-height: @navbar-collapse-max-height; + + @media (max-device-width: @screen-xs-min) and (orientation: landscape) { + max-height: 200px; + } + } +} + + +// Both navbar header and collapse +// +// When a container is present, change the behavior of the header and collapse. + +.container, +.container-fluid { + > .navbar-header, + > .navbar-collapse { + margin-right: -@navbar-padding-horizontal; + margin-left: -@navbar-padding-horizontal; + + @media (min-width: @grid-float-breakpoint) { + margin-right: 0; + margin-left: 0; + } + } +} + + +// +// Navbar alignment options +// +// Display the navbar across the entirety of the page or fixed it to the top or +// bottom of the page. + +// Static top (unfixed, but 100% wide) navbar +.navbar-static-top { + z-index: @zindex-navbar; + border-width: 0 0 1px; + + @media (min-width: @grid-float-breakpoint) { + border-radius: 0; + } +} + +// Fix the top/bottom navbars when screen real estate supports it +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: @zindex-navbar-fixed; + + // Undo the rounded corners + @media (min-width: @grid-float-breakpoint) { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; // override .navbar defaults + border-width: 1px 0 0; +} + + +// Brand/project name + +.navbar-brand { + float: left; + padding: @navbar-padding-vertical @navbar-padding-horizontal; + font-size: @font-size-large; + line-height: @line-height-computed; + height: @navbar-height; + + &:hover, + &:focus { + text-decoration: none; + } + + > img { + display: block; + } + + @media (min-width: @grid-float-breakpoint) { + .navbar > .container &, + .navbar > .container-fluid & { + margin-left: -@navbar-padding-horizontal; + } + } +} + + +// Navbar toggle +// +// Custom button for toggling the `.navbar-collapse`, powered by the collapse +// JavaScript plugin. + +.navbar-toggle { + position: relative; + float: right; + margin-right: @navbar-padding-horizontal; + padding: 9px 10px; + .navbar-vertical-align(34px); + background-color: transparent; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + border-radius: @border-radius-base; + + // We remove the `outline` here, but later compensate by attaching `:hover` + // styles to `:focus`. + &:focus { + outline: 0; + } + + // Bars + .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; + } + .icon-bar + .icon-bar { + margin-top: 4px; + } + + @media (min-width: @grid-float-breakpoint) { + display: none; + } +} + + +// Navbar nav links +// +// Builds on top of the `.nav` components with its own modifier class to make +// the nav the full height of the horizontal nav (above 768px). + +.navbar-nav { + margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal; + + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: @line-height-computed; + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + > li > a, + .dropdown-header { + padding: 5px 15px 5px 25px; + } + > li > a { + line-height: @line-height-computed; + &:hover, + &:focus { + background-image: none; + } + } + } + } + + // Uncollapse the nav + @media (min-width: @grid-float-breakpoint) { + float: left; + margin: 0; + + > li { + float: left; + > a { + padding-top: @navbar-padding-vertical; + padding-bottom: @navbar-padding-vertical; + } + } + } +} + + +// Navbar form +// +// Extension of the `.form-inline` with some extra flavor for optimum display in +// our navbars. + +.navbar-form { + margin-left: -@navbar-padding-horizontal; + margin-right: -@navbar-padding-horizontal; + padding: 10px @navbar-padding-horizontal; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); + .box-shadow(@shadow); + + // Mixin behavior for optimum display + .form-inline(); + + .form-group { + @media (max-width: @grid-float-breakpoint-max) { + margin-bottom: 5px; + + &:last-child { + margin-bottom: 0; + } + } + } + + // Vertically center in expanded, horizontal navbar + .navbar-vertical-align(@input-height-base); + + // Undo 100% width for pull classes + @media (min-width: @grid-float-breakpoint) { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + .box-shadow(none); + } +} + + +// Dropdown menus + +// Menu position and menu carets +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + .border-top-radius(0); +} +// Menu position and menu caret support for dropups via extra dropup class +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + .border-top-radius(@navbar-border-radius); + .border-bottom-radius(0); +} + + +// Buttons in navbars +// +// Vertically center a button within a navbar (when *not* in a form). + +.navbar-btn { + .navbar-vertical-align(@input-height-base); + + &.btn-sm { + .navbar-vertical-align(@input-height-small); + } + &.btn-xs { + .navbar-vertical-align(22); + } +} + + +// Text in navbars +// +// Add a class to make any element properly align itself vertically within the navbars. + +.navbar-text { + .navbar-vertical-align(@line-height-computed); + + @media (min-width: @grid-float-breakpoint) { + float: left; + margin-left: @navbar-padding-horizontal; + margin-right: @navbar-padding-horizontal; + } +} + + +// Component alignment +// +// Repurpose the pull utilities as their own navbar utilities to avoid specificity +// issues with parents and chaining. Only do this when the navbar is uncollapsed +// though so that navbar contents properly stack and align in mobile. +// +// Declared after the navbar components to ensure more specificity on the margins. + +@media (min-width: @grid-float-breakpoint) { + .navbar-left { .pull-left(); } + .navbar-right { + .pull-right(); + margin-right: -@navbar-padding-horizontal; + + ~ .navbar-right { + margin-right: 0; + } + } +} + + +// Alternate navbars +// -------------------------------------------------- + +// Default navbar +.navbar-default { + background-color: @navbar-default-bg; + border-color: @navbar-default-border; + + .navbar-brand { + color: @navbar-default-brand-color; + &:hover, + &:focus { + color: @navbar-default-brand-hover-color; + background-color: @navbar-default-brand-hover-bg; + } + } + + .navbar-text { + color: @navbar-default-color; + } + + .navbar-nav { + > li > a { + color: @navbar-default-link-color; + + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + background-color: @navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-active-color; + background-color: @navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + background-color: @navbar-default-link-disabled-bg; + } + } + } + + .navbar-toggle { + border-color: @navbar-default-toggle-border-color; + &:hover, + &:focus { + background-color: @navbar-default-toggle-hover-bg; + } + .icon-bar { + background-color: @navbar-default-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: @navbar-default-border; + } + + // Dropdown menu items + .navbar-nav { + // Remove background color from open dropdown + > .open > a { + &, + &:hover, + &:focus { + background-color: @navbar-default-link-active-bg; + color: @navbar-default-link-active-color; + } + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + > li > a { + color: @navbar-default-link-color; + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + background-color: @navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-active-color; + background-color: @navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + background-color: @navbar-default-link-disabled-bg; + } + } + } + } + } + + + // Links in navbars + // + // Add a class to ensure links outside the navbar nav are colored correctly. + + .navbar-link { + color: @navbar-default-link-color; + &:hover { + color: @navbar-default-link-hover-color; + } + } + + .btn-link { + color: @navbar-default-link-color; + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + } + } + } +} + +// Inverse navbar + +.navbar-inverse { + background-color: @navbar-inverse-bg; + border-color: @navbar-inverse-border; + + .navbar-brand { + color: @navbar-inverse-brand-color; + &:hover, + &:focus { + color: @navbar-inverse-brand-hover-color; + background-color: @navbar-inverse-brand-hover-bg; + } + } + + .navbar-text { + color: @navbar-inverse-color; + } + + .navbar-nav { + > li > a { + color: @navbar-inverse-link-color; + + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + background-color: @navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-active-color; + background-color: @navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + background-color: @navbar-inverse-link-disabled-bg; + } + } + } + + // Darken the responsive nav toggle + .navbar-toggle { + border-color: @navbar-inverse-toggle-border-color; + &:hover, + &:focus { + background-color: @navbar-inverse-toggle-hover-bg; + } + .icon-bar { + background-color: @navbar-inverse-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: darken(@navbar-inverse-bg, 7%); + } + + // Dropdowns + .navbar-nav { + > .open > a { + &, + &:hover, + &:focus { + background-color: @navbar-inverse-link-active-bg; + color: @navbar-inverse-link-active-color; + } + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display + .open .dropdown-menu { + > .dropdown-header { + border-color: @navbar-inverse-border; + } + .divider { + background-color: @navbar-inverse-border; + } + > li > a { + color: @navbar-inverse-link-color; + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + background-color: @navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-active-color; + background-color: @navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + background-color: @navbar-inverse-link-disabled-bg; + } + } + } + } + } + + .navbar-link { + color: @navbar-inverse-link-color; + &:hover { + color: @navbar-inverse-link-hover-color; + } + } + + .btn-link { + color: @navbar-inverse-link-color; + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + } + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/navs.less b/include/thirdparty/Bootstrap3/less/navs.less new file mode 100644 index 0000000..a3d11b1 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/navs.less @@ -0,0 +1,242 @@ +// +// Navs +// -------------------------------------------------- + + +// Base class +// -------------------------------------------------- + +.nav { + margin-bottom: 0; + padding-left: 0; // Override default ul/ol + list-style: none; + &:extend(.clearfix all); + + > li { + position: relative; + display: block; + + > a { + position: relative; + display: block; + padding: @nav-link-padding; + &:hover, + &:focus { + text-decoration: none; + background-color: @nav-link-hover-bg; + } + } + + // Disabled state sets text to gray and nukes hover/tab effects + &.disabled > a { + color: @nav-disabled-link-color; + + &:hover, + &:focus { + color: @nav-disabled-link-hover-color; + text-decoration: none; + background-color: transparent; + cursor: @cursor-disabled; + } + } + } + + // Open dropdowns + .open > a { + &, + &:hover, + &:focus { + background-color: @nav-link-hover-bg; + border-color: @link-color; + } + } + + // Nav dividers (deprecated with v3.0.1) + // + // This should have been removed in v3 with the dropping of `.nav-list`, but + // we missed it. We don't currently support this anywhere, but in the interest + // of maintaining backward compatibility in case you use it, it's deprecated. + .nav-divider { + .nav-divider(); + } + + // Prevent IE8 from misplacing imgs + // + // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 + > li > a > img { + max-width: none; + } +} + + +// Tabs +// ------------------------- + +// Give the tabs something to sit on +.nav-tabs { + border-bottom: 1px solid @nav-tabs-border-color; + > li { + float: left; + // Make the list-items overlay the bottom border + margin-bottom: -1px; + + // Actual tabs (as links) + > a { + margin-right: 2px; + line-height: @line-height-base; + border: 1px solid transparent; + border-radius: @border-radius-base @border-radius-base 0 0; + &:hover { + border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color; + } + } + + // Active state, and its :hover to override normal :hover + &.active > a { + &, + &:hover, + &:focus { + color: @nav-tabs-active-link-hover-color; + background-color: @nav-tabs-active-link-hover-bg; + border: 1px solid @nav-tabs-active-link-hover-border-color; + border-bottom-color: transparent; + cursor: default; + } + } + } + // pulling this in mainly for less shorthand + &.nav-justified { + .nav-justified(); + .nav-tabs-justified(); + } +} + + +// Pills +// ------------------------- +.nav-pills { + > li { + float: left; + + // Links rendered as pills + > a { + border-radius: @nav-pills-border-radius; + } + + li { + margin-left: 2px; + } + + // Active state + &.active > a { + &, + &:hover, + &:focus { + color: @nav-pills-active-link-hover-color; + background-color: @nav-pills-active-link-hover-bg; + } + } + } +} + + +// Stacked pills +.nav-stacked { + > li { + float: none; + + li { + margin-top: 2px; + margin-left: 0; // no need for this gap between nav items + } + } +} + + +// Nav variations +// -------------------------------------------------- + +// Justified nav links +// ------------------------- + +.nav-justified { + width: 100%; + + > li { + float: none; + > a { + text-align: center; + margin-bottom: 5px; + } + } + + > .dropdown .dropdown-menu { + top: auto; + left: auto; + } + + @media (min-width: @screen-sm-min) { + > li { + display: table-cell; + width: 1%; + > a { + margin-bottom: 0; + } + } + } +} + +// Move borders to anchors instead of bottom of list +// +// Mixin for adding on top the shared `.nav-justified` styles for our tabs +.nav-tabs-justified { + border-bottom: 0; + + > li > a { + // Override margin from .nav-tabs + margin-right: 0; + border-radius: @border-radius-base; + } + + > .active > a, + > .active > a:hover, + > .active > a:focus { + border: 1px solid @nav-tabs-justified-link-border-color; + } + + @media (min-width: @screen-sm-min) { + > li > a { + border-bottom: 1px solid @nav-tabs-justified-link-border-color; + border-radius: @border-radius-base @border-radius-base 0 0; + } + > .active > a, + > .active > a:hover, + > .active > a:focus { + border-bottom-color: @nav-tabs-justified-active-link-border-color; + } + } +} + + +// Tabbable tabs +// ------------------------- + +// Hide tabbable panes to start, show them when `.active` +.tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } +} + + +// Dropdowns +// ------------------------- + +// Specific dropdowns +.nav-tabs .dropdown-menu { + // make dropdown border overlap tab border + margin-top: -1px; + // Remove the top rounded corners here since there is a hard edge above the menu + .border-top-radius(0); +} diff --git a/include/thirdparty/Bootstrap3/less/normalize.less b/include/thirdparty/Bootstrap3/less/normalize.less new file mode 100644 index 0000000..9dddf73 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/normalize.less @@ -0,0 +1,424 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS and IE text size adjust after device orientation change, +// without disabling user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 +} + +// +// Remove default margin. +// + +body { + margin: 0; +} + +// HTML5 display definitions +// ========================================================================== + +// +// Correct `block` display not defined for any HTML5 element in IE 8/9. +// Correct `block` display not defined for `details` or `summary` in IE 10/11 +// and Firefox. +// Correct `block` display not defined for `main` in IE 11. +// + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +// +// 1. Correct `inline-block` display not defined in IE 8/9. +// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. +// + +audio, +canvas, +progress, +video { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// +// Prevent modern browsers from displaying `audio` without controls. +// Remove excess height in iOS 5 devices. +// + +audio:not([controls]) { + display: none; + height: 0; +} + +// +// Address `[hidden]` styling not present in IE 8/9/10. +// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. +// + +[hidden], +template { + display: none; +} + +// Links +// ========================================================================== + +// +// Remove the gray background color from active links in IE 10. +// + +a { + background-color: transparent; +} + +// +// Improve readability of focused elements when they are also in an +// active/hover state. +// + +a:active, +a:hover { + outline: 0; +} + +// Text-level semantics +// ========================================================================== + +// +// Address styling not present in IE 8/9/10/11, Safari, and Chrome. +// + +abbr[title] { + border-bottom: 1px dotted; +} + +// +// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. +// + +b, +strong { + font-weight: bold; +} + +// +// Address styling not present in Safari and Chrome. +// + +dfn { + font-style: italic; +} + +// +// Address variable `h1` font-size and margin within `section` and `article` +// contexts in Firefox 4+, Safari, and Chrome. +// + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +// +// Address styling not present in IE 8/9. +// + +mark { + background: #ff0; + color: #000; +} + +// +// Address inconsistent and variable font size in all browsers. +// + +small { + font-size: 80%; +} + +// +// Prevent `sub` and `sup` affecting `line-height` in all browsers. +// + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +// Embedded content +// ========================================================================== + +// +// Remove border when inside `a` element in IE 8/9/10. +// + +img { + border: 0; +} + +// +// Correct overflow not hidden in IE 9/10/11. +// + +svg:not(:root) { + overflow: hidden; +} + +// Grouping content +// ========================================================================== + +// +// Address margin not present in IE 8/9 and Safari. +// + +figure { + margin: 1em 40px; +} + +// +// Address differences between Firefox and other browsers. +// + +hr { + box-sizing: content-box; + height: 0; +} + +// +// Contain overflow in all browsers. +// + +pre { + overflow: auto; +} + +// +// Address odd `em`-unit font size rendering in all browsers. +// + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +// Forms +// ========================================================================== + +// +// Known limitation: by default, Chrome and Safari on OS X allow very limited +// styling of `select`, unless a `border` property is set. +// + +// +// 1. Correct color not being inherited. +// Known issue: affects color of disabled elements. +// 2. Correct font properties not being inherited. +// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. +// + +button, +input, +optgroup, +select, +textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 +} + +// +// Address `overflow` set to `hidden` in IE 8/9/10/11. +// + +button { + overflow: visible; +} + +// +// Address inconsistent `text-transform` inheritance for `button` and `select`. +// All other form control elements do not inherit `text-transform` values. +// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. +// Correct `select` style inheritance in Firefox. +// + +button, +select { + text-transform: none; +} + +// +// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +// and `video` controls. +// 2. Correct inability to style clickable `input` types in iOS. +// 3. Improve usability and consistency of cursor style between image-type +// `input` and others. +// + +button, +html input[type="button"], // 1 +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 +} + +// +// Re-set default cursor for disabled elements. +// + +button[disabled], +html input[disabled] { + cursor: default; +} + +// +// Remove inner padding and border in Firefox 4+. +// + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +// +// Address Firefox 4+ setting `line-height` on `input` using `!important` in +// the UA stylesheet. +// + +input { + line-height: normal; +} + +// +// It's recommended that you don't attempt to style these elements. +// Firefox's implementation doesn't respect box-sizing, padding, or width. +// +// 1. Address box sizing set to `content-box` in IE 8/9/10. +// 2. Remove excess padding in IE 8/9/10. +// + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 +} + +// +// Fix the cursor style for Chrome's increment/decrement buttons. For certain +// `font-size` values of the `input`, it causes the cursor style of the +// decrement button to change from `default` to `text`. +// + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +// +// 1. Address `appearance` set to `searchfield` in Safari and Chrome. +// 2. Address `box-sizing` set to `border-box` in Safari and Chrome. +// + +input[type="search"] { + -webkit-appearance: textfield; // 1 + box-sizing: content-box; //2 +} + +// +// Remove inner padding and search cancel button in Safari and Chrome on OS X. +// Safari (but not Chrome) clips the cancel button when the search input has +// padding (and `textfield` appearance). +// + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// +// Define consistent border, margin, and padding. +// + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +// +// 1. Correct `color` not being inherited in IE 8/9/10/11. +// 2. Remove padding so people aren't caught out if they zero out fieldsets. +// + +legend { + border: 0; // 1 + padding: 0; // 2 +} + +// +// Remove default vertical scrollbar in IE 8/9/10/11. +// + +textarea { + overflow: auto; +} + +// +// Don't inherit the `font-weight` (applied by a rule above). +// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. +// + +optgroup { + font-weight: bold; +} + +// Tables +// ========================================================================== + +// +// Remove most spacing between table cells. +// + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/include/thirdparty/Bootstrap3/less/pager.less b/include/thirdparty/Bootstrap3/less/pager.less new file mode 100644 index 0000000..41abaaa --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/pager.less @@ -0,0 +1,54 @@ +// +// Pager pagination +// -------------------------------------------------- + + +.pager { + padding-left: 0; + margin: @line-height-computed 0; + list-style: none; + text-align: center; + &:extend(.clearfix all); + li { + display: inline; + > a, + > span { + display: inline-block; + padding: 5px 14px; + background-color: @pager-bg; + border: 1px solid @pager-border; + border-radius: @pager-border-radius; + } + + > a:hover, + > a:focus { + text-decoration: none; + background-color: @pager-hover-bg; + } + } + + .next { + > a, + > span { + float: right; + } + } + + .previous { + > a, + > span { + float: left; + } + } + + .disabled { + > a, + > a:hover, + > a:focus, + > span { + color: @pager-disabled-color; + background-color: @pager-bg; + cursor: @cursor-disabled; + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/pagination.less b/include/thirdparty/Bootstrap3/less/pagination.less new file mode 100644 index 0000000..31f77aa --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/pagination.less @@ -0,0 +1,89 @@ +// +// Pagination (multiple pages) +// -------------------------------------------------- +.pagination { + display: inline-block; + padding-left: 0; + margin: @line-height-computed 0; + border-radius: @border-radius-base; + + > li { + display: inline; // Remove list-style and block-level defaults + > a, + > span { + position: relative; + float: left; // Collapse white-space + padding: @padding-base-vertical @padding-base-horizontal; + line-height: @line-height-base; + text-decoration: none; + color: @pagination-color; + background-color: @pagination-bg; + border: 1px solid @pagination-border; + margin-left: -1px; + } + &:first-child { + > a, + > span { + margin-left: 0; + .border-left-radius(@border-radius-base); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius-base); + } + } + } + + > li > a, + > li > span { + &:hover, + &:focus { + z-index: 2; + color: @pagination-hover-color; + background-color: @pagination-hover-bg; + border-color: @pagination-hover-border; + } + } + + > .active > a, + > .active > span { + &, + &:hover, + &:focus { + z-index: 3; + color: @pagination-active-color; + background-color: @pagination-active-bg; + border-color: @pagination-active-border; + cursor: default; + } + } + + > .disabled { + > span, + > span:hover, + > span:focus, + > a, + > a:hover, + > a:focus { + color: @pagination-disabled-color; + background-color: @pagination-disabled-bg; + border-color: @pagination-disabled-border; + cursor: @cursor-disabled; + } + } +} + +// Sizing +// -------------------------------------------------- + +// Large +.pagination-lg { + .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); +} + +// Small +.pagination-sm { + .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} diff --git a/include/thirdparty/Bootstrap3/less/panels.less b/include/thirdparty/Bootstrap3/less/panels.less new file mode 100644 index 0000000..425eb5e --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/panels.less @@ -0,0 +1,271 @@ +// +// Panels +// -------------------------------------------------- + + +// Base class +.panel { + margin-bottom: @line-height-computed; + background-color: @panel-bg; + border: 1px solid transparent; + border-radius: @panel-border-radius; + .box-shadow(0 1px 1px rgba(0,0,0,.05)); +} + +// Panel contents +.panel-body { + padding: @panel-body-padding; + &:extend(.clearfix all); +} + +// Optional heading +.panel-heading { + padding: @panel-heading-padding; + border-bottom: 1px solid transparent; + .border-top-radius((@panel-border-radius - 1)); + + > .dropdown .dropdown-toggle { + color: inherit; + } +} + +// Within heading, strip any `h*` tag of its default margins for spacing. +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: ceil((@font-size-base * 1.125)); + color: inherit; + + > a, + > small, + > .small, + > small > a, + > .small > a { + color: inherit; + } +} + +// Optional footer (stays gray in every modifier class) +.panel-footer { + padding: @panel-footer-padding; + background-color: @panel-footer-bg; + border-top: 1px solid @panel-inner-border; + .border-bottom-radius((@panel-border-radius - 1)); +} + + +// List groups in panels +// +// By default, space out list group content from panel headings to account for +// any kind of custom content between the two. + +.panel { + > .list-group, + > .panel-collapse > .list-group { + margin-bottom: 0; + + .list-group-item { + border-width: 1px 0; + border-radius: 0; + } + + // Add border top radius for first one + &:first-child { + .list-group-item:first-child { + border-top: 0; + .border-top-radius((@panel-border-radius - 1)); + } + } + + // Add border bottom radius for last one + &:last-child { + .list-group-item:last-child { + border-bottom: 0; + .border-bottom-radius((@panel-border-radius - 1)); + } + } + } + > .panel-heading + .panel-collapse > .list-group { + .list-group-item:first-child { + .border-top-radius(0); + } + } +} +// Collapse space between when there's no additional content. +.panel-heading + .list-group { + .list-group-item:first-child { + border-top-width: 0; + } +} +.list-group + .panel-footer { + border-top-width: 0; +} + +// Tables in panels +// +// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and +// watch it go full width. + +.panel { + > .table, + > .table-responsive > .table, + > .panel-collapse > .table { + margin-bottom: 0; + + caption { + padding-left: @panel-body-padding; + padding-right: @panel-body-padding; + } + } + // Add border top radius for first one + > .table:first-child, + > .table-responsive:first-child > .table:first-child { + .border-top-radius((@panel-border-radius - 1)); + + > thead:first-child, + > tbody:first-child { + > tr:first-child { + border-top-left-radius: (@panel-border-radius - 1); + border-top-right-radius: (@panel-border-radius - 1); + + td:first-child, + th:first-child { + border-top-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-top-right-radius: (@panel-border-radius - 1); + } + } + } + } + // Add border bottom radius for last one + > .table:last-child, + > .table-responsive:last-child > .table:last-child { + .border-bottom-radius((@panel-border-radius - 1)); + + > tbody:last-child, + > tfoot:last-child { + > tr:last-child { + border-bottom-left-radius: (@panel-border-radius - 1); + border-bottom-right-radius: (@panel-border-radius - 1); + + td:first-child, + th:first-child { + border-bottom-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-bottom-right-radius: (@panel-border-radius - 1); + } + } + } + } + > .panel-body + .table, + > .panel-body + .table-responsive, + > .table + .panel-body, + > .table-responsive + .panel-body { + border-top: 1px solid @table-border-color; + } + > .table > tbody:first-child > tr:first-child th, + > .table > tbody:first-child > tr:first-child td { + border-top: 0; + } + > .table-bordered, + > .table-responsive > .table-bordered { + border: 0; + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + > thead, + > tbody { + > tr:first-child { + > td, + > th { + border-bottom: 0; + } + } + } + > tbody, + > tfoot { + > tr:last-child { + > td, + > th { + border-bottom: 0; + } + } + } + } + > .table-responsive { + border: 0; + margin-bottom: 0; + } +} + + +// Collapsable panels (aka, accordion) +// +// Wrap a series of panels in `.panel-group` to turn them into an accordion with +// the help of our collapse JavaScript plugin. + +.panel-group { + margin-bottom: @line-height-computed; + + // Tighten up margin so it's only between panels + .panel { + margin-bottom: 0; + border-radius: @panel-border-radius; + + + .panel { + margin-top: 5px; + } + } + + .panel-heading { + border-bottom: 0; + + + .panel-collapse > .panel-body, + + .panel-collapse > .list-group { + border-top: 1px solid @panel-inner-border; + } + } + + .panel-footer { + border-top: 0; + + .panel-collapse .panel-body { + border-bottom: 1px solid @panel-inner-border; + } + } +} + + +// Contextual variations +.panel-default { + .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border); +} +.panel-primary { + .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border); +} +.panel-success { + .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border); +} +.panel-info { + .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border); +} +.panel-warning { + .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border); +} +.panel-danger { + .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border); +} diff --git a/include/thirdparty/Bootstrap3/less/popovers.less b/include/thirdparty/Bootstrap3/less/popovers.less new file mode 100644 index 0000000..3a62a64 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/popovers.less @@ -0,0 +1,131 @@ +// +// Popovers +// -------------------------------------------------- + + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: @zindex-popover; + display: none; + max-width: @popover-max-width; + padding: 1px; + // Our parent element can be arbitrary since popovers are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + .reset-text(); + font-size: @font-size-base; + + background-color: @popover-bg; + background-clip: padding-box; + border: 1px solid @popover-fallback-border-color; + border: 1px solid @popover-border-color; + border-radius: @border-radius-large; + .box-shadow(0 5px 10px rgba(0,0,0,.2)); + + // Offset the popover to account for the popover arrow + &.top { margin-top: -@popover-arrow-width; } + &.right { margin-left: @popover-arrow-width; } + &.bottom { margin-top: @popover-arrow-width; } + &.left { margin-left: -@popover-arrow-width; } +} + +.popover-title { + margin: 0; // reset heading margin + padding: 8px 14px; + font-size: @font-size-base; + background-color: @popover-title-bg; + border-bottom: 1px solid darken(@popover-title-bg, 5%); + border-radius: (@border-radius-large - 1) (@border-radius-large - 1) 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +// Arrows +// +// .arrow is outer, .arrow:after is inner + +.popover > .arrow { + &, + &:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } +} +.popover > .arrow { + border-width: @popover-arrow-outer-width; +} +.popover > .arrow:after { + border-width: @popover-arrow-width; + content: ""; +} + +.popover { + &.top > .arrow { + left: 50%; + margin-left: -@popover-arrow-outer-width; + border-bottom-width: 0; + border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-top-color: @popover-arrow-outer-color; + bottom: -@popover-arrow-outer-width; + &:after { + content: " "; + bottom: 1px; + margin-left: -@popover-arrow-width; + border-bottom-width: 0; + border-top-color: @popover-arrow-color; + } + } + &.right > .arrow { + top: 50%; + left: -@popover-arrow-outer-width; + margin-top: -@popover-arrow-outer-width; + border-left-width: 0; + border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-right-color: @popover-arrow-outer-color; + &:after { + content: " "; + left: 1px; + bottom: -@popover-arrow-width; + border-left-width: 0; + border-right-color: @popover-arrow-color; + } + } + &.bottom > .arrow { + left: 50%; + margin-left: -@popover-arrow-outer-width; + border-top-width: 0; + border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-bottom-color: @popover-arrow-outer-color; + top: -@popover-arrow-outer-width; + &:after { + content: " "; + top: 1px; + margin-left: -@popover-arrow-width; + border-top-width: 0; + border-bottom-color: @popover-arrow-color; + } + } + + &.left > .arrow { + top: 50%; + right: -@popover-arrow-outer-width; + margin-top: -@popover-arrow-outer-width; + border-right-width: 0; + border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-left-color: @popover-arrow-outer-color; + &:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: @popover-arrow-color; + bottom: -@popover-arrow-width; + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/print.less b/include/thirdparty/Bootstrap3/less/print.less new file mode 100644 index 0000000..66e54ab --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/print.less @@ -0,0 +1,101 @@ +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ + +// ========================================================================== +// Print styles. +// Inlined to avoid the additional HTTP request: h5bp.com/r +// ========================================================================== + +@media print { + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; // Black prints faster: h5bp.com/s + box-shadow: none !important; + text-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + // Don't show links that are fragment identifiers, + // or use the `javascript:` pseudo protocol + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; // h5bp.com/t + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + // Bootstrap specific changes start + + // Bootstrap components + .navbar { + display: none; + } + .btn, + .dropup > .btn { + > .caret { + border-top-color: #000 !important; + } + } + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + + td, + th { + background-color: #fff !important; + } + } + .table-bordered { + th, + td { + border: 1px solid #ddd !important; + } + } + + // Bootstrap specific changes end +} diff --git a/include/thirdparty/Bootstrap3/less/progress-bars.less b/include/thirdparty/Bootstrap3/less/progress-bars.less new file mode 100644 index 0000000..8868a1f --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/progress-bars.less @@ -0,0 +1,87 @@ +// +// Progress bars +// -------------------------------------------------- + + +// Bar animations +// ------------------------- + +// WebKit +@-webkit-keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + +// Spec and IE10+ +@keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + + +// Bar itself +// ------------------------- + +// Outer container +.progress { + overflow: hidden; + height: @line-height-computed; + margin-bottom: @line-height-computed; + background-color: @progress-bg; + border-radius: @progress-border-radius; + .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); +} + +// Bar of progress +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: @font-size-small; + line-height: @line-height-computed; + color: @progress-bar-color; + text-align: center; + background-color: @progress-bar-bg; + .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); + .transition(width .6s ease); +} + +// Striped bars +// +// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the +// `.progress-bar-striped` class, which you just add to an existing +// `.progress-bar`. +.progress-striped .progress-bar, +.progress-bar-striped { + #gradient > .striped(); + background-size: 40px 40px; +} + +// Call animation for the active one +// +// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the +// `.progress-bar.active` approach. +.progress.active .progress-bar, +.progress-bar.active { + .animation(progress-bar-stripes 2s linear infinite); +} + + +// Variations +// ------------------------- + +.progress-bar-success { + .progress-bar-variant(@progress-bar-success-bg); +} + +.progress-bar-info { + .progress-bar-variant(@progress-bar-info-bg); +} + +.progress-bar-warning { + .progress-bar-variant(@progress-bar-warning-bg); +} + +.progress-bar-danger { + .progress-bar-variant(@progress-bar-danger-bg); +} diff --git a/include/thirdparty/Bootstrap3/less/responsive-embed.less b/include/thirdparty/Bootstrap3/less/responsive-embed.less new file mode 100644 index 0000000..080a511 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/responsive-embed.less @@ -0,0 +1,35 @@ +// Embeds responsive +// +// Credit: Nicolas Gallagher and SUIT CSS. + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; + + .embed-responsive-item, + iframe, + embed, + object, + video { + position: absolute; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 100%; + border: 0; + } +} + +// Modifier class for 16:9 aspect ratio +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} + +// Modifier class for 4:3 aspect ratio +.embed-responsive-4by3 { + padding-bottom: 75%; +} diff --git a/include/thirdparty/Bootstrap3/less/responsive-utilities.less b/include/thirdparty/Bootstrap3/less/responsive-utilities.less new file mode 100644 index 0000000..b1db31d --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/responsive-utilities.less @@ -0,0 +1,194 @@ +// +// Responsive: Utility classes +// -------------------------------------------------- + + +// IE10 in Windows (Phone) 8 +// +// Support for responsive views via media queries is kind of borked in IE10, for +// Surface/desktop in split view and for Windows Phone 8. This particular fix +// must be accompanied by a snippet of JavaScript to sniff the user agent and +// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at +// our Getting Started page for more information on this bug. +// +// For more information, see the following: +// +// Issue: https://github.com/twbs/bootstrap/issues/10497 +// Docs: http://getbootstrap.com/getting-started/#support-ie10-width +// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/ +// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ + +@-ms-viewport { + width: device-width; +} + + +// Visibility utilities +// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0 +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + .responsive-invisibility(); +} + +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} + +.visible-xs { + @media (max-width: @screen-xs-max) { + .responsive-visibility(); + } +} +.visible-xs-block { + @media (max-width: @screen-xs-max) { + display: block !important; + } +} +.visible-xs-inline { + @media (max-width: @screen-xs-max) { + display: inline !important; + } +} +.visible-xs-inline-block { + @media (max-width: @screen-xs-max) { + display: inline-block !important; + } +} + +.visible-sm { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + .responsive-visibility(); + } +} +.visible-sm-block { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + display: block !important; + } +} +.visible-sm-inline { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + display: inline !important; + } +} +.visible-sm-inline-block { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + display: inline-block !important; + } +} + +.visible-md { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + .responsive-visibility(); + } +} +.visible-md-block { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + display: block !important; + } +} +.visible-md-inline { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + display: inline !important; + } +} +.visible-md-inline-block { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + display: inline-block !important; + } +} + +.visible-lg { + @media (min-width: @screen-lg-min) { + .responsive-visibility(); + } +} +.visible-lg-block { + @media (min-width: @screen-lg-min) { + display: block !important; + } +} +.visible-lg-inline { + @media (min-width: @screen-lg-min) { + display: inline !important; + } +} +.visible-lg-inline-block { + @media (min-width: @screen-lg-min) { + display: inline-block !important; + } +} + +.hidden-xs { + @media (max-width: @screen-xs-max) { + .responsive-invisibility(); + } +} +.hidden-sm { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + .responsive-invisibility(); + } +} +.hidden-md { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + .responsive-invisibility(); + } +} +.hidden-lg { + @media (min-width: @screen-lg-min) { + .responsive-invisibility(); + } +} + + +// Print utilities +// +// Media queries are placed on the inside to be mixin-friendly. + +// Note: Deprecated .visible-print as of v3.2.0 +.visible-print { + .responsive-invisibility(); + + @media print { + .responsive-visibility(); + } +} +.visible-print-block { + display: none !important; + + @media print { + display: block !important; + } +} +.visible-print-inline { + display: none !important; + + @media print { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; + + @media print { + display: inline-block !important; + } +} + +.hidden-print { + @media print { + .responsive-invisibility(); + } +} diff --git a/include/thirdparty/Bootstrap3/less/scaffolding.less b/include/thirdparty/Bootstrap3/less/scaffolding.less new file mode 100644 index 0000000..1929bfc --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/scaffolding.less @@ -0,0 +1,161 @@ +// +// Scaffolding +// -------------------------------------------------- + + +// Reset the box-sizing +// +// Heads up! This reset may cause conflicts with some third-party widgets. +// For recommendations on resolving such conflicts, see +// http://getbootstrap.com/getting-started/#third-box-sizing +* { + .box-sizing(border-box); +} +*:before, +*:after { + .box-sizing(border-box); +} + + +// Body reset + +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +body { + font-family: @font-family-base; + font-size: @font-size-base; + line-height: @line-height-base; + color: @text-color; + background-color: @body-bg; +} + +// Reset fonts for relevant elements +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + + +// Links + +a { + color: @link-color; + text-decoration: none; + + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: @link-hover-decoration; + } + + &:focus { + .tab-focus(); + } +} + + +// Figures +// +// We reset this here because previously Normalize had no `figure` margins. This +// ensures we don't break anyone's use of the element. + +figure { + margin: 0; +} + + +// Images + +img { + vertical-align: middle; +} + +// Responsive images (ensure images don't scale beyond their parents) +.img-responsive { + .img-responsive(); +} + +// Rounded corners +.img-rounded { + border-radius: @border-radius-large; +} + +// Image thumbnails +// +// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. +.img-thumbnail { + padding: @thumbnail-padding; + line-height: @line-height-base; + background-color: @thumbnail-bg; + border: 1px solid @thumbnail-border; + border-radius: @thumbnail-border-radius; + .transition(all .2s ease-in-out); + + // Keep them at most 100% wide + .img-responsive(inline-block); +} + +// Perfect circle +.img-circle { + border-radius: 50%; // set radius in percents +} + + +// Horizontal rules + +hr { + margin-top: @line-height-computed; + margin-bottom: @line-height-computed; + border: 0; + border-top: 1px solid @hr-border; +} + + +// Only display content to screen readers +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// Credit: HTML5 Boilerplate + +.sr-only-focusable { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} + + +// iOS "clickable elements" fix for role="button" +// +// Fixes "clickability" issue (and more generally, the firing of events such as focus as well) +// for traditionally non-focusable elements with role="button" +// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +[role="button"] { + cursor: pointer; +} diff --git a/include/thirdparty/Bootstrap3/less/tables.less b/include/thirdparty/Bootstrap3/less/tables.less new file mode 100644 index 0000000..2242c03 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/tables.less @@ -0,0 +1,234 @@ +// +// Tables +// -------------------------------------------------- + + +table { + background-color: @table-bg; +} +caption { + padding-top: @table-cell-padding; + padding-bottom: @table-cell-padding; + color: @text-muted; + text-align: left; +} +th { + text-align: left; +} + + +// Baseline styles + +.table { + width: 100%; + max-width: 100%; + margin-bottom: @line-height-computed; + // Cells + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-cell-padding; + line-height: @line-height-base; + vertical-align: top; + border-top: 1px solid @table-border-color; + } + } + } + // Bottom align for column headings + > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid @table-border-color; + } + // Remove top border from thead by default + > caption + thead, + > colgroup + thead, + > thead:first-child { + > tr:first-child { + > th, + > td { + border-top: 0; + } + } + } + // Account for multiple tbody instances + > tbody + tbody { + border-top: 2px solid @table-border-color; + } + + // Nesting + .table { + background-color: @body-bg; + } +} + + +// Condensed table w/ half padding + +.table-condensed { + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-condensed-cell-padding; + } + } + } +} + + +// Bordered version +// +// Add borders all around the table and between all the columns. + +.table-bordered { + border: 1px solid @table-border-color; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + border: 1px solid @table-border-color; + } + } + } + > thead > tr { + > th, + > td { + border-bottom-width: 2px; + } + } +} + + +// Zebra-striping +// +// Default zebra-stripe styles (alternating gray and transparent backgrounds) + +.table-striped { + > tbody > tr:nth-of-type(odd) { + background-color: @table-bg-accent; + } +} + + +// Hover effect +// +// Placed here since it has to come after the potential zebra striping + +.table-hover { + > tbody > tr:hover { + background-color: @table-bg-hover; + } +} + + +// Table cell sizing +// +// Reset default table behavior + +table col[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-column; +} +table { + td, + th { + &[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-cell; + } + } +} + + +// Table backgrounds +// +// Exact selectors below required to override `.table-striped` and prevent +// inheritance to nested tables. + +// Generate the contextual variants +.table-row-variant(active; @table-bg-active); +.table-row-variant(success; @state-success-bg); +.table-row-variant(info; @state-info-bg); +.table-row-variant(warning; @state-warning-bg); +.table-row-variant(danger; @state-danger-bg); + + +// Responsive tables +// +// Wrap your tables in `.table-responsive` and we'll make them mobile friendly +// by enabling horizontal scrolling. Only applies <768px. Everything above that +// will display normally. + +.table-responsive { + overflow-x: auto; + min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837) + + @media screen and (max-width: @screen-xs-max) { + width: 100%; + margin-bottom: (@line-height-computed * 0.75); + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid @table-border-color; + + // Tighten up spacing + > .table { + margin-bottom: 0; + + // Ensure the content doesn't wrap + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + white-space: nowrap; + } + } + } + } + + // Special overrides for the bordered tables + > .table-bordered { + border: 0; + + // Nuke the appropriate borders so that the parent can handle them + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + + // Only nuke the last row's bottom-border in `tbody` and `tfoot` since + // chances are there will be only one `tr` in a `thead` and that would + // remove the border altogether. + > tbody, + > tfoot { + > tr:last-child { + > th, + > td { + border-bottom: 0; + } + } + } + + } + } +} diff --git a/include/thirdparty/Bootstrap3/less/theme.less b/include/thirdparty/Bootstrap3/less/theme.less new file mode 100644 index 0000000..8f51d91 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/theme.less @@ -0,0 +1,291 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +// +// Load core variables and mixins +// -------------------------------------------------- + +@import "variables.less"; +@import "mixins.less"; + + +// +// Buttons +// -------------------------------------------------- + +// Common styles +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0,0,0,.2); + @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); + .box-shadow(@shadow); + + // Reset the shadow + &:active, + &.active { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + .box-shadow(none); + } + + .badge { + text-shadow: none; + } +} + +// Mixin for generating new styles +.btn-styles(@btn-color: #555) { + #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%)); + .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620 + background-repeat: repeat-x; + border-color: darken(@btn-color, 14%); + + &:hover, + &:focus { + background-color: darken(@btn-color, 12%); + background-position: 0 -15px; + } + + &:active, + &.active { + background-color: darken(@btn-color, 12%); + border-color: darken(@btn-color, 14%); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &.focus, + &:active, + &.active { + background-color: darken(@btn-color, 12%); + background-image: none; + } + } +} + +// Common styles +.btn { + // Remove the gradient for the pressed/active state + &:active, + &.active { + background-image: none; + } +} + +// Apply the mixin to the buttons +.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } +.btn-primary { .btn-styles(@btn-primary-bg); } +.btn-success { .btn-styles(@btn-success-bg); } +.btn-info { .btn-styles(@btn-info-bg); } +.btn-warning { .btn-styles(@btn-warning-bg); } +.btn-danger { .btn-styles(@btn-danger-bg); } + + +// +// Images +// -------------------------------------------------- + +.thumbnail, +.img-thumbnail { + .box-shadow(0 1px 2px rgba(0,0,0,.075)); +} + + +// +// Dropdowns +// -------------------------------------------------- + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%)); + background-color: darken(@dropdown-link-hover-bg, 5%); +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); + background-color: darken(@dropdown-link-active-bg, 5%); +} + + +// +// Navbar +// -------------------------------------------------- + +// Default navbar +.navbar-default { + #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg); + .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + border-radius: @navbar-border-radius; + @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); + .box-shadow(@shadow); + + .navbar-nav > .open > a, + .navbar-nav > .active > a { + #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%)); + .box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); + } +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255,255,255,.25); +} + +// Inverted navbar +.navbar-inverse { + #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg); + .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257 + border-radius: @navbar-border-radius; + .navbar-nav > .open > a, + .navbar-nav > .active > a { + #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%)); + .box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); + } + + .navbar-brand, + .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } +} + +// Undo rounded corners in static and fixed navbars +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} + +// Fix active state of dropdown items in collapsed mode +@media (max-width: @grid-float-breakpoint-max) { + .navbar .navbar-nav .open .dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: #fff; + #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); + } + } +} + + +// +// Alerts +// -------------------------------------------------- + +// Common styles +.alert { + text-shadow: 0 1px 0 rgba(255,255,255,.2); + @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); + .box-shadow(@shadow); +} + +// Mixin for generating new styles +.alert-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%)); + border-color: darken(@color, 15%); +} + +// Apply the mixin to the alerts +.alert-success { .alert-styles(@alert-success-bg); } +.alert-info { .alert-styles(@alert-info-bg); } +.alert-warning { .alert-styles(@alert-warning-bg); } +.alert-danger { .alert-styles(@alert-danger-bg); } + + +// +// Progress bars +// -------------------------------------------------- + +// Give the progress background some depth +.progress { + #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg) +} + +// Mixin for generating new styles +.progress-bar-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%)); +} + +// Apply the mixin to the progress bars +.progress-bar { .progress-bar-styles(@progress-bar-bg); } +.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); } +.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); } +.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); } +.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); } + +// Reset the striped class because our mixins don't do multiple gradients and +// the above custom styles override the new `.progress-bar-striped` in v3.2.0. +.progress-bar-striped { + #gradient > .striped(); +} + + +// +// List groups +// -------------------------------------------------- + +.list-group { + border-radius: @border-radius-base; + .box-shadow(0 1px 2px rgba(0,0,0,.075)); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%); + #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%)); + border-color: darken(@list-group-active-border, 7.5%); + + .badge { + text-shadow: none; + } +} + + +// +// Panels +// -------------------------------------------------- + +// Common styles +.panel { + .box-shadow(0 1px 2px rgba(0,0,0,.05)); +} + +// Mixin for generating new styles +.panel-heading-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%)); +} + +// Apply the mixin to the panel headings only +.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); } +.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); } +.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); } +.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); } +.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); } +.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); } + + +// +// Wells +// -------------------------------------------------- + +.well { + #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg); + border-color: darken(@well-bg, 10%); + @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); + .box-shadow(@shadow); +} diff --git a/include/thirdparty/Bootstrap3/less/thumbnails.less b/include/thirdparty/Bootstrap3/less/thumbnails.less new file mode 100644 index 0000000..0713e67 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/thumbnails.less @@ -0,0 +1,36 @@ +// +// Thumbnails +// -------------------------------------------------- + + +// Mixin and adjust the regular image class +.thumbnail { + display: block; + padding: @thumbnail-padding; + margin-bottom: @line-height-computed; + line-height: @line-height-base; + background-color: @thumbnail-bg; + border: 1px solid @thumbnail-border; + border-radius: @thumbnail-border-radius; + .transition(border .2s ease-in-out); + + > img, + a > img { + &:extend(.img-responsive); + margin-left: auto; + margin-right: auto; + } + + // Add a hover state for linked versions only + a&:hover, + a&:focus, + a&.active { + border-color: @link-color; + } + + // Image captions + .caption { + padding: @thumbnail-caption-padding; + color: @thumbnail-caption-color; + } +} diff --git a/include/thirdparty/Bootstrap3/less/tooltip.less b/include/thirdparty/Bootstrap3/less/tooltip.less new file mode 100644 index 0000000..b48d63e --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/tooltip.less @@ -0,0 +1,101 @@ +// +// Tooltips +// -------------------------------------------------- + + +// Base class +.tooltip { + position: absolute; + z-index: @zindex-tooltip; + display: block; + // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + .reset-text(); + font-size: @font-size-small; + + .opacity(0); + + &.in { .opacity(@tooltip-opacity); } + &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; } + &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; } + &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; } + &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; } +} + +// Wrapper for the tooltip content +.tooltip-inner { + max-width: @tooltip-max-width; + padding: 3px 8px; + color: @tooltip-color; + text-align: center; + background-color: @tooltip-bg; + border-radius: @border-radius-base; +} + +// Arrows +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1 +.tooltip { + &.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.top-left .tooltip-arrow { + bottom: 0; + right: @tooltip-arrow-width; + margin-bottom: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.top-right .tooltip-arrow { + bottom: 0; + left: @tooltip-arrow-width; + margin-bottom: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; + border-right-color: @tooltip-arrow-color; + } + &.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; + border-left-color: @tooltip-arrow-color; + } + &.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } + &.bottom-left .tooltip-arrow { + top: 0; + right: @tooltip-arrow-width; + margin-top: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } + &.bottom-right .tooltip-arrow { + top: 0; + left: @tooltip-arrow-width; + margin-top: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } +} diff --git a/include/thirdparty/Bootstrap3/less/type.less b/include/thirdparty/Bootstrap3/less/type.less new file mode 100644 index 0000000..0d4fee4 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/type.less @@ -0,0 +1,302 @@ +// +// Typography +// -------------------------------------------------- + + +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: @headings-font-family; + font-weight: @headings-font-weight; + line-height: @headings-line-height; + color: @headings-color; + + small, + .small { + font-weight: normal; + line-height: 1; + color: @headings-small-color; + } +} + +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: @line-height-computed; + margin-bottom: (@line-height-computed / 2); + + small, + .small { + font-size: 65%; + } +} +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: (@line-height-computed / 2); + margin-bottom: (@line-height-computed / 2); + + small, + .small { + font-size: 75%; + } +} + +h1, .h1 { font-size: @font-size-h1; } +h2, .h2 { font-size: @font-size-h2; } +h3, .h3 { font-size: @font-size-h3; } +h4, .h4 { font-size: @font-size-h4; } +h5, .h5 { font-size: @font-size-h5; } +h6, .h6 { font-size: @font-size-h6; } + + +// Body text +// ------------------------- + +p { + margin: 0 0 (@line-height-computed / 2); +} + +.lead { + margin-bottom: @line-height-computed; + font-size: floor((@font-size-base * 1.15)); + font-weight: 300; + line-height: 1.4; + + @media (min-width: @screen-sm-min) { + font-size: (@font-size-base * 1.5); + } +} + + +// Emphasis & misc +// ------------------------- + +// Ex: (12px small font / 14px base font) * 100% = about 85% +small, +.small { + font-size: floor((100% * @font-size-small / @font-size-base)); +} + +mark, +.mark { + background-color: @state-warning-bg; + padding: .2em; +} + +// Alignment +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-justify { text-align: justify; } +.text-nowrap { white-space: nowrap; } + +// Transformation +.text-lowercase { text-transform: lowercase; } +.text-uppercase { text-transform: uppercase; } +.text-capitalize { text-transform: capitalize; } + +// Contextual colors +.text-muted { + color: @text-muted; +} +.text-primary { + .text-emphasis-variant(@brand-primary); +} +.text-success { + .text-emphasis-variant(@state-success-text); +} +.text-info { + .text-emphasis-variant(@state-info-text); +} +.text-warning { + .text-emphasis-variant(@state-warning-text); +} +.text-danger { + .text-emphasis-variant(@state-danger-text); +} + +// Contextual backgrounds +// For now we'll leave these alongside the text classes until v4 when we can +// safely shift things around (per SemVer rules). +.bg-primary { + // Given the contrast here, this is the only class to have its color inverted + // automatically. + color: #fff; + .bg-variant(@brand-primary); +} +.bg-success { + .bg-variant(@state-success-bg); +} +.bg-info { + .bg-variant(@state-info-bg); +} +.bg-warning { + .bg-variant(@state-warning-bg); +} +.bg-danger { + .bg-variant(@state-danger-bg); +} + + +// Page header +// ------------------------- + +.page-header { + padding-bottom: ((@line-height-computed / 2) - 1); + margin: (@line-height-computed * 2) 0 @line-height-computed; + border-bottom: 1px solid @page-header-border-color; +} + + +// Lists +// ------------------------- + +// Unordered and Ordered lists +ul, +ol { + margin-top: 0; + margin-bottom: (@line-height-computed / 2); + ul, + ol { + margin-bottom: 0; + } +} + +// List options + +// Unstyled keeps list items block level, just removes default browser padding and list-style +.list-unstyled { + padding-left: 0; + list-style: none; +} + +// Inline turns list items into inline-block +.list-inline { + .list-unstyled(); + margin-left: -5px; + + > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; + } +} + +// Description Lists +dl { + margin-top: 0; // Remove browser default + margin-bottom: @line-height-computed; +} +dt, +dd { + line-height: @line-height-base; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; // Undo browser default +} + +// Horizontal description lists +// +// Defaults to being stacked without any of the below styles applied, until the +// grid breakpoint is reached (default of ~768px). + +.dl-horizontal { + dd { + &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present + } + + @media (min-width: @dl-horizontal-breakpoint) { + dt { + float: left; + width: (@dl-horizontal-offset - 20); + clear: left; + text-align: right; + .text-overflow(); + } + dd { + margin-left: @dl-horizontal-offset; + } + } +} + + +// Misc +// ------------------------- + +// Abbreviations and acronyms +abbr[title], +// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted @abbr-border-color; +} +.initialism { + font-size: 90%; + .text-uppercase(); +} + +// Blockquotes +blockquote { + padding: (@line-height-computed / 2) @line-height-computed; + margin: 0 0 @line-height-computed; + font-size: @blockquote-font-size; + border-left: 5px solid @blockquote-border-color; + + p, + ul, + ol { + &:last-child { + margin-bottom: 0; + } + } + + // Note: Deprecated small and .small as of v3.1.0 + // Context: https://github.com/twbs/bootstrap/issues/11660 + footer, + small, + .small { + display: block; + font-size: 80%; // back to default font-size + line-height: @line-height-base; + color: @blockquote-small-color; + + &:before { + content: '\2014 \00A0'; // em dash, nbsp + } + } +} + +// Opposite alignment of blockquote +// +// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid @blockquote-border-color; + border-left: 0; + text-align: right; + + // Account for citation + footer, + small, + .small { + &:before { content: ''; } + &:after { + content: '\00A0 \2014'; // nbsp, em dash + } + } +} + +// Addresses +address { + margin-bottom: @line-height-computed; + font-style: normal; + line-height: @line-height-base; +} diff --git a/include/thirdparty/Bootstrap3/less/utilities.less b/include/thirdparty/Bootstrap3/less/utilities.less new file mode 100644 index 0000000..7a8ca27 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/utilities.less @@ -0,0 +1,55 @@ +// +// Utility classes +// -------------------------------------------------- + + +// Floats +// ------------------------- + +.clearfix { + .clearfix(); +} +.center-block { + .center-block(); +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} + + +// Toggling content +// ------------------------- + +// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + .text-hide(); +} + + +// Hide from screenreaders and browsers +// +// Credit: HTML5 Boilerplate + +.hidden { + display: none !important; +} + + +// For Affix plugin +// ------------------------- + +.affix { + position: fixed; +} diff --git a/include/thirdparty/Bootstrap3/less/variables.less b/include/thirdparty/Bootstrap3/less/variables.less new file mode 100644 index 0000000..b057ef5 --- /dev/null +++ b/include/thirdparty/Bootstrap3/less/variables.less @@ -0,0 +1,869 @@ +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +@gray-base: #000; +@gray-darker: lighten(@gray-base, 13.5%); // #222 +@gray-dark: lighten(@gray-base, 20%); // #333 +@gray: lighten(@gray-base, 33.5%); // #555 +@gray-light: lighten(@gray-base, 46.7%); // #777 +@gray-lighter: lighten(@gray-base, 93.5%); // #eee + +@brand-primary: darken(#428bca, 6.5%); // #337ab7 +@brand-success: #5cb85c; +@brand-info: #5bc0de; +@brand-warning: #f0ad4e; +@brand-danger: #d9534f; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +@body-bg: #fff; +//** Global text color on ``. +@text-color: @gray-dark; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); +//** Link hover decoration. +@link-hover-decoration: underline; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for ``, ``, and `
    `.
    +@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
    +@font-family-base:        @font-family-sans-serif;
    +
    +@font-size-base:          14px;
    +@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
    +@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
    +
    +@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
    +@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
    +@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
    +@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
    +@font-size-h5:            @font-size-base;
    +@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
    +
    +//** Unit-less `line-height` for use in components like buttons.
    +@line-height-base:        1.428571429; // 20/14
    +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
    +@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
    +
    +//** By default, this inherits from the ``.
    +@headings-font-family:    inherit;
    +@headings-font-weight:    500;
    +@headings-line-height:    1.1;
    +@headings-color:          inherit;
    +
    +
    +//== Iconography
    +//
    +//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
    +
    +//** Load fonts from this directory.
    +@icon-font-path:          "../fonts/";
    +//** File name for all font files.
    +@icon-font-name:          "glyphicons-halflings-regular";
    +//** Element ID within SVG icon file.
    +@icon-font-svg-id:        "glyphicons_halflingsregular";
    +
    +
    +//== Components
    +//
    +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
    +
    +@padding-base-vertical:     6px;
    +@padding-base-horizontal:   12px;
    +
    +@padding-large-vertical:    10px;
    +@padding-large-horizontal:  16px;
    +
    +@padding-small-vertical:    5px;
    +@padding-small-horizontal:  10px;
    +
    +@padding-xs-vertical:       1px;
    +@padding-xs-horizontal:     5px;
    +
    +@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
    +@line-height-small:         1.5;
    +
    +@border-radius-base:        4px;
    +@border-radius-large:       6px;
    +@border-radius-small:       3px;
    +
    +//** Global color for active items (e.g., navs or dropdowns).
    +@component-active-color:    #fff;
    +//** Global background color for active items (e.g., navs or dropdowns).
    +@component-active-bg:       @brand-primary;
    +
    +//** Width of the `border` for generating carets that indicator dropdowns.
    +@caret-width-base:          4px;
    +//** Carets increase slightly in size for larger components.
    +@caret-width-large:         5px;
    +
    +
    +//== Tables
    +//
    +//## Customizes the `.table` component with basic values, each used across all table variations.
    +
    +//** Padding for ``s and ``s.
    +@table-cell-padding:            8px;
    +//** Padding for cells in `.table-condensed`.
    +@table-condensed-cell-padding:  5px;
    +
    +//** Default background color used for all tables.
    +@table-bg:                      transparent;
    +//** Background color used for `.table-striped`.
    +@table-bg-accent:               #f9f9f9;
    +//** Background color used for `.table-hover`.
    +@table-bg-hover:                #f5f5f5;
    +@table-bg-active:               @table-bg-hover;
    +
    +//** Border color for table and cell borders.
    +@table-border-color:            #ddd;
    +
    +
    +//== Buttons
    +//
    +//## For each of Bootstrap's buttons, define text, background and border color.
    +
    +@btn-font-weight:                normal;
    +
    +@btn-default-color:              #333;
    +@btn-default-bg:                 #fff;
    +@btn-default-border:             #ccc;
    +
    +@btn-primary-color:              #fff;
    +@btn-primary-bg:                 @brand-primary;
    +@btn-primary-border:             darken(@btn-primary-bg, 5%);
    +
    +@btn-success-color:              #fff;
    +@btn-success-bg:                 @brand-success;
    +@btn-success-border:             darken(@btn-success-bg, 5%);
    +
    +@btn-info-color:                 #fff;
    +@btn-info-bg:                    @brand-info;
    +@btn-info-border:                darken(@btn-info-bg, 5%);
    +
    +@btn-warning-color:              #fff;
    +@btn-warning-bg:                 @brand-warning;
    +@btn-warning-border:             darken(@btn-warning-bg, 5%);
    +
    +@btn-danger-color:               #fff;
    +@btn-danger-bg:                  @brand-danger;
    +@btn-danger-border:              darken(@btn-danger-bg, 5%);
    +
    +@btn-link-disabled-color:        @gray-light;
    +
    +// Allows for customizing button radius independently from global border radius
    +@btn-border-radius-base:         @border-radius-base;
    +@btn-border-radius-large:        @border-radius-large;
    +@btn-border-radius-small:        @border-radius-small;
    +
    +
    +//== Forms
    +//
    +//##
    +
    +//** `` background color
    +@input-bg:                       #fff;
    +//** `` background color
    +@input-bg-disabled:              @gray-lighter;
    +
    +//** Text color for ``s
    +@input-color:                    @gray;
    +//** `` border color
    +@input-border:                   #ccc;
    +
    +// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
    +//** Default `.form-control` border radius
    +// This has no effect on ``s in CSS.
    +@input-border-radius:            @border-radius-base;
    +//** Large `.form-control` border radius
    +@input-border-radius-large:      @border-radius-large;
    +//** Small `.form-control` border radius
    +@input-border-radius-small:      @border-radius-small;
    +
    +//** Border color for inputs on focus
    +@input-border-focus:             #66afe9;
    +
    +//** Placeholder text color
    +@input-color-placeholder:        #999;
    +
    +//** Default `.form-control` height
    +@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
    +//** Large `.form-control` height
    +@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
    +//** Small `.form-control` height
    +@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
    +
    +//** `.form-group` margin
    +@form-group-margin-bottom:       15px;
    +
    +@legend-color:                   @gray-dark;
    +@legend-border-color:            #e5e5e5;
    +
    +//** Background color for textual input addons
    +@input-group-addon-bg:           @gray-lighter;
    +//** Border color for textual input addons
    +@input-group-addon-border-color: @input-border;
    +
    +//** Disabled cursor for form controls and buttons.
    +@cursor-disabled:                not-allowed;
    +
    +
    +//== Dropdowns
    +//
    +//## Dropdown menu container and contents.
    +
    +//** Background for the dropdown menu.
    +@dropdown-bg:                    #fff;
    +//** Dropdown menu `border-color`.
    +@dropdown-border:                rgba(0,0,0,.15);
    +//** Dropdown menu `border-color` **for IE8**.
    +@dropdown-fallback-border:       #ccc;
    +//** Divider color for between dropdown items.
    +@dropdown-divider-bg:            #e5e5e5;
    +
    +//** Dropdown link text color.
    +@dropdown-link-color:            @gray-dark;
    +//** Hover color for dropdown links.
    +@dropdown-link-hover-color:      darken(@gray-dark, 5%);
    +//** Hover background for dropdown links.
    +@dropdown-link-hover-bg:         #f5f5f5;
    +
    +//** Active dropdown menu item text color.
    +@dropdown-link-active-color:     @component-active-color;
    +//** Active dropdown menu item background color.
    +@dropdown-link-active-bg:        @component-active-bg;
    +
    +//** Disabled dropdown menu item background color.
    +@dropdown-link-disabled-color:   @gray-light;
    +
    +//** Text color for headers within dropdown menus.
    +@dropdown-header-color:          @gray-light;
    +
    +//** Deprecated `@dropdown-caret-color` as of v3.1.0
    +@dropdown-caret-color:           #000;
    +
    +
    +//-- Z-index master list
    +//
    +// Warning: Avoid customizing these values. They're used for a bird's eye view
    +// of components dependent on the z-axis and are designed to all work together.
    +//
    +// Note: These variables are not generated into the Customizer.
    +
    +@zindex-navbar:            1000;
    +@zindex-dropdown:          1000;
    +@zindex-popover:           1060;
    +@zindex-tooltip:           1070;
    +@zindex-navbar-fixed:      1030;
    +@zindex-modal-background:  1040;
    +@zindex-modal:             1050;
    +
    +
    +//== Media queries breakpoints
    +//
    +//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
    +
    +// Extra small screen / phone
    +//** Deprecated `@screen-xs` as of v3.0.1
    +@screen-xs:                  480px;
    +//** Deprecated `@screen-xs-min` as of v3.2.0
    +@screen-xs-min:              @screen-xs;
    +//** Deprecated `@screen-phone` as of v3.0.1
    +@screen-phone:               @screen-xs-min;
    +
    +// Small screen / tablet
    +//** Deprecated `@screen-sm` as of v3.0.1
    +@screen-sm:                  768px;
    +@screen-sm-min:              @screen-sm;
    +//** Deprecated `@screen-tablet` as of v3.0.1
    +@screen-tablet:              @screen-sm-min;
    +
    +// Medium screen / desktop
    +//** Deprecated `@screen-md` as of v3.0.1
    +@screen-md:                  992px;
    +@screen-md-min:              @screen-md;
    +//** Deprecated `@screen-desktop` as of v3.0.1
    +@screen-desktop:             @screen-md-min;
    +
    +// Large screen / wide desktop
    +//** Deprecated `@screen-lg` as of v3.0.1
    +@screen-lg:                  1200px;
    +@screen-lg-min:              @screen-lg;
    +//** Deprecated `@screen-lg-desktop` as of v3.0.1
    +@screen-lg-desktop:          @screen-lg-min;
    +
    +// So media queries don't overlap when required, provide a maximum
    +@screen-xs-max:              (@screen-sm-min - 1);
    +@screen-sm-max:              (@screen-md-min - 1);
    +@screen-md-max:              (@screen-lg-min - 1);
    +
    +
    +//== Grid system
    +//
    +//## Define your custom responsive grid.
    +
    +//** Number of columns in the grid.
    +@grid-columns:              12;
    +//** Padding between columns. Gets divided in half for the left and right.
    +@grid-gutter-width:         30px;
    +// Navbar collapse
    +//** Point at which the navbar becomes uncollapsed.
    +@grid-float-breakpoint:     @screen-sm-min;
    +//** Point at which the navbar begins collapsing.
    +@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
    +
    +
    +//== Container sizes
    +//
    +//## Define the maximum width of `.container` for different screen sizes.
    +
    +// Small screen / tablet
    +@container-tablet:             (720px + @grid-gutter-width);
    +//** For `@screen-sm-min` and up.
    +@container-sm:                 @container-tablet;
    +
    +// Medium screen / desktop
    +@container-desktop:            (940px + @grid-gutter-width);
    +//** For `@screen-md-min` and up.
    +@container-md:                 @container-desktop;
    +
    +// Large screen / wide desktop
    +@container-large-desktop:      (1140px + @grid-gutter-width);
    +//** For `@screen-lg-min` and up.
    +@container-lg:                 @container-large-desktop;
    +
    +
    +//== Navbar
    +//
    +//##
    +
    +// Basics of a navbar
    +@navbar-height:                    50px;
    +@navbar-margin-bottom:             @line-height-computed;
    +@navbar-border-radius:             @border-radius-base;
    +@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
    +@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
    +@navbar-collapse-max-height:       340px;
    +
    +@navbar-default-color:             #777;
    +@navbar-default-bg:                #f8f8f8;
    +@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
    +
    +// Navbar links
    +@navbar-default-link-color:                #777;
    +@navbar-default-link-hover-color:          #333;
    +@navbar-default-link-hover-bg:             transparent;
    +@navbar-default-link-active-color:         #555;
    +@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
    +@navbar-default-link-disabled-color:       #ccc;
    +@navbar-default-link-disabled-bg:          transparent;
    +
    +// Navbar brand label
    +@navbar-default-brand-color:               @navbar-default-link-color;
    +@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
    +@navbar-default-brand-hover-bg:            transparent;
    +
    +// Navbar toggle
    +@navbar-default-toggle-hover-bg:           #ddd;
    +@navbar-default-toggle-icon-bar-bg:        #888;
    +@navbar-default-toggle-border-color:       #ddd;
    +
    +
    +//=== Inverted navbar
    +// Reset inverted navbar basics
    +@navbar-inverse-color:                      lighten(@gray-light, 15%);
    +@navbar-inverse-bg:                         #222;
    +@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
    +
    +// Inverted navbar links
    +@navbar-inverse-link-color:                 lighten(@gray-light, 15%);
    +@navbar-inverse-link-hover-color:           #fff;
    +@navbar-inverse-link-hover-bg:              transparent;
    +@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
    +@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
    +@navbar-inverse-link-disabled-color:        #444;
    +@navbar-inverse-link-disabled-bg:           transparent;
    +
    +// Inverted navbar brand label
    +@navbar-inverse-brand-color:                @navbar-inverse-link-color;
    +@navbar-inverse-brand-hover-color:          #fff;
    +@navbar-inverse-brand-hover-bg:             transparent;
    +
    +// Inverted navbar toggle
    +@navbar-inverse-toggle-hover-bg:            #333;
    +@navbar-inverse-toggle-icon-bar-bg:         #fff;
    +@navbar-inverse-toggle-border-color:        #333;
    +
    +
    +//== Navs
    +//
    +//##
    +
    +//=== Shared nav styles
    +@nav-link-padding:                          10px 15px;
    +@nav-link-hover-bg:                         @gray-lighter;
    +
    +@nav-disabled-link-color:                   @gray-light;
    +@nav-disabled-link-hover-color:             @gray-light;
    +
    +//== Tabs
    +@nav-tabs-border-color:                     #ddd;
    +
    +@nav-tabs-link-hover-border-color:          @gray-lighter;
    +
    +@nav-tabs-active-link-hover-bg:             @body-bg;
    +@nav-tabs-active-link-hover-color:          @gray;
    +@nav-tabs-active-link-hover-border-color:   #ddd;
    +
    +@nav-tabs-justified-link-border-color:            #ddd;
    +@nav-tabs-justified-active-link-border-color:     @body-bg;
    +
    +//== Pills
    +@nav-pills-border-radius:                   @border-radius-base;
    +@nav-pills-active-link-hover-bg:            @component-active-bg;
    +@nav-pills-active-link-hover-color:         @component-active-color;
    +
    +
    +//== Pagination
    +//
    +//##
    +
    +@pagination-color:                     @link-color;
    +@pagination-bg:                        #fff;
    +@pagination-border:                    #ddd;
    +
    +@pagination-hover-color:               @link-hover-color;
    +@pagination-hover-bg:                  @gray-lighter;
    +@pagination-hover-border:              #ddd;
    +
    +@pagination-active-color:              #fff;
    +@pagination-active-bg:                 @brand-primary;
    +@pagination-active-border:             @brand-primary;
    +
    +@pagination-disabled-color:            @gray-light;
    +@pagination-disabled-bg:               #fff;
    +@pagination-disabled-border:           #ddd;
    +
    +
    +//== Pager
    +//
    +//##
    +
    +@pager-bg:                             @pagination-bg;
    +@pager-border:                         @pagination-border;
    +@pager-border-radius:                  15px;
    +
    +@pager-hover-bg:                       @pagination-hover-bg;
    +
    +@pager-active-bg:                      @pagination-active-bg;
    +@pager-active-color:                   @pagination-active-color;
    +
    +@pager-disabled-color:                 @pagination-disabled-color;
    +
    +
    +//== Jumbotron
    +//
    +//##
    +
    +@jumbotron-padding:              30px;
    +@jumbotron-color:                inherit;
    +@jumbotron-bg:                   @gray-lighter;
    +@jumbotron-heading-color:        inherit;
    +@jumbotron-font-size:            ceil((@font-size-base * 1.5));
    +@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
    +
    +
    +//== Form states and alerts
    +//
    +//## Define colors for form feedback states and, by default, alerts.
    +
    +@state-success-text:             #3c763d;
    +@state-success-bg:               #dff0d8;
    +@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
    +
    +@state-info-text:                #31708f;
    +@state-info-bg:                  #d9edf7;
    +@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
    +
    +@state-warning-text:             #8a6d3b;
    +@state-warning-bg:               #fcf8e3;
    +@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
    +
    +@state-danger-text:              #a94442;
    +@state-danger-bg:                #f2dede;
    +@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
    +
    +
    +//== Tooltips
    +//
    +//##
    +
    +//** Tooltip max width
    +@tooltip-max-width:           200px;
    +//** Tooltip text color
    +@tooltip-color:               #fff;
    +//** Tooltip background color
    +@tooltip-bg:                  #000;
    +@tooltip-opacity:             .9;
    +
    +//** Tooltip arrow width
    +@tooltip-arrow-width:         5px;
    +//** Tooltip arrow color
    +@tooltip-arrow-color:         @tooltip-bg;
    +
    +
    +//== Popovers
    +//
    +//##
    +
    +//** Popover body background color
    +@popover-bg:                          #fff;
    +//** Popover maximum width
    +@popover-max-width:                   276px;
    +//** Popover border color
    +@popover-border-color:                rgba(0,0,0,.2);
    +//** Popover fallback border color
    +@popover-fallback-border-color:       #ccc;
    +
    +//** Popover title background color
    +@popover-title-bg:                    darken(@popover-bg, 3%);
    +
    +//** Popover arrow width
    +@popover-arrow-width:                 10px;
    +//** Popover arrow color
    +@popover-arrow-color:                 @popover-bg;
    +
    +//** Popover outer arrow width
    +@popover-arrow-outer-width:           (@popover-arrow-width + 1);
    +//** Popover outer arrow color
    +@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
    +//** Popover outer arrow fallback color
    +@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
    +
    +
    +//== Labels
    +//
    +//##
    +
    +//** Default label background color
    +@label-default-bg:            @gray-light;
    +//** Primary label background color
    +@label-primary-bg:            @brand-primary;
    +//** Success label background color
    +@label-success-bg:            @brand-success;
    +//** Info label background color
    +@label-info-bg:               @brand-info;
    +//** Warning label background color
    +@label-warning-bg:            @brand-warning;
    +//** Danger label background color
    +@label-danger-bg:             @brand-danger;
    +
    +//** Default label text color
    +@label-color:                 #fff;
    +//** Default text color of a linked label
    +@label-link-hover-color:      #fff;
    +
    +
    +//== Modals
    +//
    +//##
    +
    +//** Padding applied to the modal body
    +@modal-inner-padding:         15px;
    +
    +//** Padding applied to the modal title
    +@modal-title-padding:         15px;
    +//** Modal title line-height
    +@modal-title-line-height:     @line-height-base;
    +
    +//** Background color of modal content area
    +@modal-content-bg:                             #fff;
    +//** Modal content border color
    +@modal-content-border-color:                   rgba(0,0,0,.2);
    +//** Modal content border color **for IE8**
    +@modal-content-fallback-border-color:          #999;
    +
    +//** Modal backdrop background color
    +@modal-backdrop-bg:           #000;
    +//** Modal backdrop opacity
    +@modal-backdrop-opacity:      .5;
    +//** Modal header border color
    +@modal-header-border-color:   #e5e5e5;
    +//** Modal footer border color
    +@modal-footer-border-color:   @modal-header-border-color;
    +
    +@modal-lg:                    900px;
    +@modal-md:                    600px;
    +@modal-sm:                    300px;
    +
    +
    +//== Alerts
    +//
    +//## Define alert colors, border radius, and padding.
    +
    +@alert-padding:               15px;
    +@alert-border-radius:         @border-radius-base;
    +@alert-link-font-weight:      bold;
    +
    +@alert-success-bg:            @state-success-bg;
    +@alert-success-text:          @state-success-text;
    +@alert-success-border:        @state-success-border;
    +
    +@alert-info-bg:               @state-info-bg;
    +@alert-info-text:             @state-info-text;
    +@alert-info-border:           @state-info-border;
    +
    +@alert-warning-bg:            @state-warning-bg;
    +@alert-warning-text:          @state-warning-text;
    +@alert-warning-border:        @state-warning-border;
    +
    +@alert-danger-bg:             @state-danger-bg;
    +@alert-danger-text:           @state-danger-text;
    +@alert-danger-border:         @state-danger-border;
    +
    +
    +//== Progress bars
    +//
    +//##
    +
    +//** Background color of the whole progress component
    +@progress-bg:                 #f5f5f5;
    +//** Progress bar text color
    +@progress-bar-color:          #fff;
    +//** Variable for setting rounded corners on progress bar.
    +@progress-border-radius:      @border-radius-base;
    +
    +//** Default progress bar color
    +@progress-bar-bg:             @brand-primary;
    +//** Success progress bar color
    +@progress-bar-success-bg:     @brand-success;
    +//** Warning progress bar color
    +@progress-bar-warning-bg:     @brand-warning;
    +//** Danger progress bar color
    +@progress-bar-danger-bg:      @brand-danger;
    +//** Info progress bar color
    +@progress-bar-info-bg:        @brand-info;
    +
    +
    +//== List group
    +//
    +//##
    +
    +//** Background color on `.list-group-item`
    +@list-group-bg:                 #fff;
    +//** `.list-group-item` border color
    +@list-group-border:             #ddd;
    +//** List group border radius
    +@list-group-border-radius:      @border-radius-base;
    +
    +//** Background color of single list items on hover
    +@list-group-hover-bg:           #f5f5f5;
    +//** Text color of active list items
    +@list-group-active-color:       @component-active-color;
    +//** Background color of active list items
    +@list-group-active-bg:          @component-active-bg;
    +//** Border color of active list elements
    +@list-group-active-border:      @list-group-active-bg;
    +//** Text color for content within active list items
    +@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
    +
    +//** Text color of disabled list items
    +@list-group-disabled-color:      @gray-light;
    +//** Background color of disabled list items
    +@list-group-disabled-bg:         @gray-lighter;
    +//** Text color for content within disabled list items
    +@list-group-disabled-text-color: @list-group-disabled-color;
    +
    +@list-group-link-color:         #555;
    +@list-group-link-hover-color:   @list-group-link-color;
    +@list-group-link-heading-color: #333;
    +
    +
    +//== Panels
    +//
    +//##
    +
    +@panel-bg:                    #fff;
    +@panel-body-padding:          15px;
    +@panel-heading-padding:       10px 15px;
    +@panel-footer-padding:        @panel-heading-padding;
    +@panel-border-radius:         @border-radius-base;
    +
    +//** Border color for elements within panels
    +@panel-inner-border:          #ddd;
    +@panel-footer-bg:             #f5f5f5;
    +
    +@panel-default-text:          @gray-dark;
    +@panel-default-border:        #ddd;
    +@panel-default-heading-bg:    #f5f5f5;
    +
    +@panel-primary-text:          #fff;
    +@panel-primary-border:        @brand-primary;
    +@panel-primary-heading-bg:    @brand-primary;
    +
    +@panel-success-text:          @state-success-text;
    +@panel-success-border:        @state-success-border;
    +@panel-success-heading-bg:    @state-success-bg;
    +
    +@panel-info-text:             @state-info-text;
    +@panel-info-border:           @state-info-border;
    +@panel-info-heading-bg:       @state-info-bg;
    +
    +@panel-warning-text:          @state-warning-text;
    +@panel-warning-border:        @state-warning-border;
    +@panel-warning-heading-bg:    @state-warning-bg;
    +
    +@panel-danger-text:           @state-danger-text;
    +@panel-danger-border:         @state-danger-border;
    +@panel-danger-heading-bg:     @state-danger-bg;
    +
    +
    +//== Thumbnails
    +//
    +//##
    +
    +//** Padding around the thumbnail image
    +@thumbnail-padding:           4px;
    +//** Thumbnail background color
    +@thumbnail-bg:                @body-bg;
    +//** Thumbnail border color
    +@thumbnail-border:            #ddd;
    +//** Thumbnail border radius
    +@thumbnail-border-radius:     @border-radius-base;
    +
    +//** Custom text color for thumbnail captions
    +@thumbnail-caption-color:     @text-color;
    +//** Padding around the thumbnail caption
    +@thumbnail-caption-padding:   9px;
    +
    +
    +//== Wells
    +//
    +//##
    +
    +@well-bg:                     #f5f5f5;
    +@well-border:                 darken(@well-bg, 7%);
    +
    +
    +//== Badges
    +//
    +//##
    +
    +@badge-color:                 #fff;
    +//** Linked badge text color on hover
    +@badge-link-hover-color:      #fff;
    +@badge-bg:                    @gray-light;
    +
    +//** Badge text color in active nav link
    +@badge-active-color:          @link-color;
    +//** Badge background color in active nav link
    +@badge-active-bg:             #fff;
    +
    +@badge-font-weight:           bold;
    +@badge-line-height:           1;
    +@badge-border-radius:         10px;
    +
    +
    +//== Breadcrumbs
    +//
    +//##
    +
    +@breadcrumb-padding-vertical:   8px;
    +@breadcrumb-padding-horizontal: 15px;
    +//** Breadcrumb background color
    +@breadcrumb-bg:                 #f5f5f5;
    +//** Breadcrumb text color
    +@breadcrumb-color:              #ccc;
    +//** Text color of current page in the breadcrumb
    +@breadcrumb-active-color:       @gray-light;
    +//** Textual separator for between breadcrumb elements
    +@breadcrumb-separator:          "/";
    +
    +
    +//== Carousel
    +//
    +//##
    +
    +@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
    +
    +@carousel-control-color:                      #fff;
    +@carousel-control-width:                      15%;
    +@carousel-control-opacity:                    .5;
    +@carousel-control-font-size:                  20px;
    +
    +@carousel-indicator-active-bg:                #fff;
    +@carousel-indicator-border-color:             #fff;
    +
    +@carousel-caption-color:                      #fff;
    +
    +
    +//== Close
    +//
    +//##
    +
    +@close-font-weight:           bold;
    +@close-color:                 #000;
    +@close-text-shadow:           0 1px 0 #fff;
    +
    +
    +//== Code
    +//
    +//##
    +
    +@code-color:                  #c7254e;
    +@code-bg:                     #f9f2f4;
    +
    +@kbd-color:                   #fff;
    +@kbd-bg:                      #333;
    +
    +@pre-bg:                      #f5f5f5;
    +@pre-color:                   @gray-dark;
    +@pre-border-color:            #ccc;
    +@pre-scrollable-max-height:   340px;
    +
    +
    +//== Type
    +//
    +//##
    +
    +//** Horizontal offset for forms and lists.
    +@component-offset-horizontal: 180px;
    +//** Text muted color
    +@text-muted:                  @gray-light;
    +//** Abbreviations and acronyms border color
    +@abbr-border-color:           @gray-light;
    +//** Headings small color
    +@headings-small-color:        @gray-light;
    +//** Blockquote small color
    +@blockquote-small-color:      @gray-light;
    +//** Blockquote font size
    +@blockquote-font-size:        (@font-size-base * 1.25);
    +//** Blockquote border color
    +@blockquote-border-color:     @gray-lighter;
    +//** Page header border color
    +@page-header-border-color:    @gray-lighter;
    +//** Width of horizontal description list titles
    +@dl-horizontal-offset:        @component-offset-horizontal;
    +//** Point at which .dl-horizontal becomes horizontal
    +@dl-horizontal-breakpoint:    @grid-float-breakpoint;
    +//** Horizontal line color.
    +@hr-border:                   @gray-lighter;
    diff --git a/include/thirdparty/Bootstrap3/less/wells.less b/include/thirdparty/Bootstrap3/less/wells.less
    new file mode 100644
    index 0000000..15d072b
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/less/wells.less
    @@ -0,0 +1,29 @@
    +//
    +// Wells
    +// --------------------------------------------------
    +
    +
    +// Base class
    +.well {
    +  min-height: 20px;
    +  padding: 19px;
    +  margin-bottom: 20px;
    +  background-color: @well-bg;
    +  border: 1px solid @well-border;
    +  border-radius: @border-radius-base;
    +  .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
    +  blockquote {
    +    border-color: #ddd;
    +    border-color: rgba(0,0,0,.15);
    +  }
    +}
    +
    +// Sizes
    +.well-lg {
    +  padding: 24px;
    +  border-radius: @border-radius-large;
    +}
    +.well-sm {
    +  padding: 9px;
    +  border-radius: @border-radius-small;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/_bootstrap.scss b/include/thirdparty/Bootstrap3/scss/_bootstrap.scss
    new file mode 100644
    index 0000000..c773c8c
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/_bootstrap.scss
    @@ -0,0 +1,56 @@
    +/*!
    + * Bootstrap v3.3.6 (http://getbootstrap.com)
    + * Copyright 2011-2015 Twitter, Inc.
    + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
    + */
    +
    +// Core variables and mixins
    +@import "bootstrap/variables";
    +@import "bootstrap/mixins";
    +
    +// Reset and dependencies
    +@import "bootstrap/normalize";
    +@import "bootstrap/print";
    +@import "bootstrap/glyphicons";
    +
    +// Core CSS
    +@import "bootstrap/scaffolding";
    +@import "bootstrap/type";
    +@import "bootstrap/code";
    +@import "bootstrap/grid";
    +@import "bootstrap/tables";
    +@import "bootstrap/forms";
    +@import "bootstrap/buttons";
    +
    +// Components
    +@import "bootstrap/component-animations";
    +@import "bootstrap/dropdowns";
    +@import "bootstrap/button-groups";
    +@import "bootstrap/input-groups";
    +@import "bootstrap/navs";
    +@import "bootstrap/navbar";
    +@import "bootstrap/breadcrumbs";
    +@import "bootstrap/pagination";
    +@import "bootstrap/pager";
    +@import "bootstrap/labels";
    +@import "bootstrap/badges";
    +@import "bootstrap/jumbotron";
    +@import "bootstrap/thumbnails";
    +@import "bootstrap/alerts";
    +@import "bootstrap/progress-bars";
    +@import "bootstrap/media";
    +@import "bootstrap/list-group";
    +@import "bootstrap/panels";
    +@import "bootstrap/responsive-embed";
    +@import "bootstrap/wells";
    +@import "bootstrap/close";
    +
    +// Components w/ JavaScript
    +@import "bootstrap/modals";
    +@import "bootstrap/tooltip";
    +@import "bootstrap/popovers";
    +@import "bootstrap/carousel";
    +
    +// Utility classes
    +@import "bootstrap/utilities";
    +@import "bootstrap/responsive-utilities";
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_alerts.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_alerts.scss
    new file mode 100644
    index 0000000..7d1e1fd
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_alerts.scss
    @@ -0,0 +1,73 @@
    +//
    +// Alerts
    +// --------------------------------------------------
    +
    +
    +// Base styles
    +// -------------------------
    +
    +.alert {
    +  padding: $alert-padding;
    +  margin-bottom: $line-height-computed;
    +  border: 1px solid transparent;
    +  border-radius: $alert-border-radius;
    +
    +  // Headings for larger alerts
    +  h4 {
    +    margin-top: 0;
    +    // Specified for the h4 to prevent conflicts of changing $headings-color
    +    color: inherit;
    +  }
    +
    +  // Provide class for links that match alerts
    +  .alert-link {
    +    font-weight: $alert-link-font-weight;
    +  }
    +
    +  // Improve alignment and spacing of inner content
    +  > p,
    +  > ul {
    +    margin-bottom: 0;
    +  }
    +
    +  > p + p {
    +    margin-top: 5px;
    +  }
    +}
    +
    +// Dismissible alerts
    +//
    +// Expand the right padding and account for the close button's positioning.
    +
    +.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.
    +.alert-dismissible {
    +  padding-right: ($alert-padding + 20);
    +
    +  // Adjust close link position
    +  .close {
    +    position: relative;
    +    top: -2px;
    +    right: -21px;
    +    color: inherit;
    +  }
    +}
    +
    +// Alternate styles
    +//
    +// Generate contextual modifier classes for colorizing the alert.
    +
    +.alert-success {
    +  @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text);
    +}
    +
    +.alert-info {
    +  @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text);
    +}
    +
    +.alert-warning {
    +  @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text);
    +}
    +
    +.alert-danger {
    +  @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text);
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_badges.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_badges.scss
    new file mode 100644
    index 0000000..70002e0
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_badges.scss
    @@ -0,0 +1,68 @@
    +//
    +// Badges
    +// --------------------------------------------------
    +
    +
    +// Base class
    +.badge {
    +  display: inline-block;
    +  min-width: 10px;
    +  padding: 3px 7px;
    +  font-size: $font-size-small;
    +  font-weight: $badge-font-weight;
    +  color: $badge-color;
    +  line-height: $badge-line-height;
    +  vertical-align: middle;
    +  white-space: nowrap;
    +  text-align: center;
    +  background-color: $badge-bg;
    +  border-radius: $badge-border-radius;
    +
    +  // Empty badges collapse automatically (not available in IE8)
    +  &:empty {
    +    display: none;
    +  }
    +
    +  // Quick fix for badges in buttons
    +  .btn & {
    +    position: relative;
    +    top: -1px;
    +  }
    +
    +  .btn-xs &,
    +  .btn-group-xs > .btn & {
    +    top: 0;
    +    padding: 1px 5px;
    +  }
    +
    +  // [converter] extracted a& to a.badge
    +
    +  // Account for badges in navs
    +  .list-group-item.active > &,
    +  .nav-pills > .active > a > & {
    +    color: $badge-active-color;
    +    background-color: $badge-active-bg;
    +  }
    +
    +  .list-group-item > & {
    +    float: right;
    +  }
    +
    +  .list-group-item > & + & {
    +    margin-right: 5px;
    +  }
    +
    +  .nav-pills > li > a > & {
    +    margin-left: 3px;
    +  }
    +}
    +
    +// Hover state, but only for links
    +a.badge {
    +  &:hover,
    +  &:focus {
    +    color: $badge-link-hover-color;
    +    text-decoration: none;
    +    cursor: pointer;
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_breadcrumbs.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_breadcrumbs.scss
    new file mode 100644
    index 0000000..b61f0c7
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_breadcrumbs.scss
    @@ -0,0 +1,28 @@
    +//
    +// Breadcrumbs
    +// --------------------------------------------------
    +
    +
    +.breadcrumb {
    +  padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal;
    +  margin-bottom: $line-height-computed;
    +  list-style: none;
    +  background-color: $breadcrumb-bg;
    +  border-radius: $border-radius-base;
    +
    +  > li {
    +    display: inline-block;
    +
    +    + li:before {
    +      // [converter] Workaround for https://github.com/sass/libsass/issues/1115
    +      $nbsp: "\00a0";
    +      content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space
    +      padding: 0 5px;
    +      color: $breadcrumb-color;
    +    }
    +  }
    +
    +  > .active {
    +    color: $breadcrumb-active-color;
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_button-groups.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_button-groups.scss
    new file mode 100644
    index 0000000..baaacc4
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_button-groups.scss
    @@ -0,0 +1,244 @@
    +//
    +// Button groups
    +// --------------------------------------------------
    +
    +// Make the div behave like a button
    +.btn-group,
    +.btn-group-vertical {
    +  position: relative;
    +  display: inline-block;
    +  vertical-align: middle; // match .btn alignment given font-size hack above
    +  > .btn {
    +    position: relative;
    +    float: left;
    +    // Bring the "active" button to the front
    +    &:hover,
    +    &:focus,
    +    &:active,
    +    &.active {
    +      z-index: 2;
    +    }
    +  }
    +}
    +
    +// Prevent double borders when buttons are next to each other
    +.btn-group {
    +  .btn + .btn,
    +  .btn + .btn-group,
    +  .btn-group + .btn,
    +  .btn-group + .btn-group {
    +    margin-left: -1px;
    +  }
    +}
    +
    +// Optional: Group multiple button groups together for a toolbar
    +.btn-toolbar {
    +  margin-left: -5px; // Offset the first child's margin
    +  @include clearfix;
    +
    +  .btn,
    +  .btn-group,
    +  .input-group {
    +    float: left;
    +  }
    +  > .btn,
    +  > .btn-group,
    +  > .input-group {
    +    margin-left: 5px;
    +  }
    +}
    +
    +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
    +  border-radius: 0;
    +}
    +
    +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match
    +.btn-group > .btn:first-child {
    +  margin-left: 0;
    +  &:not(:last-child):not(.dropdown-toggle) {
    +    @include border-right-radius(0);
    +  }
    +}
    +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it
    +.btn-group > .btn:last-child:not(:first-child),
    +.btn-group > .dropdown-toggle:not(:first-child) {
    +  @include border-left-radius(0);
    +}
    +
    +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)
    +.btn-group > .btn-group {
    +  float: left;
    +}
    +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
    +  border-radius: 0;
    +}
    +.btn-group > .btn-group:first-child:not(:last-child) {
    +  > .btn:last-child,
    +  > .dropdown-toggle {
    +    @include border-right-radius(0);
    +  }
    +}
    +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {
    +  @include border-left-radius(0);
    +}
    +
    +// On active and open, don't show outline
    +.btn-group .dropdown-toggle:active,
    +.btn-group.open .dropdown-toggle {
    +  outline: 0;
    +}
    +
    +
    +// Sizing
    +//
    +// Remix the default button sizing classes into new ones for easier manipulation.
    +
    +.btn-group-xs > .btn { @extend .btn-xs; }
    +.btn-group-sm > .btn { @extend .btn-sm; }
    +.btn-group-lg > .btn { @extend .btn-lg; }
    +
    +
    +// Split button dropdowns
    +// ----------------------
    +
    +// Give the line between buttons some depth
    +.btn-group > .btn + .dropdown-toggle {
    +  padding-left: 8px;
    +  padding-right: 8px;
    +}
    +.btn-group > .btn-lg + .dropdown-toggle {
    +  padding-left: 12px;
    +  padding-right: 12px;
    +}
    +
    +// The clickable button for toggling the menu
    +// Remove the gradient and set the same inset shadow as the :active state
    +.btn-group.open .dropdown-toggle {
    +  @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
    +
    +  // Show no shadow for `.btn-link` since it has no other button styles.
    +  &.btn-link {
    +    @include box-shadow(none);
    +  }
    +}
    +
    +
    +// Reposition the caret
    +.btn .caret {
    +  margin-left: 0;
    +}
    +// Carets in other button sizes
    +.btn-lg .caret {
    +  border-width: $caret-width-large $caret-width-large 0;
    +  border-bottom-width: 0;
    +}
    +// Upside down carets for .dropup
    +.dropup .btn-lg .caret {
    +  border-width: 0 $caret-width-large $caret-width-large;
    +}
    +
    +
    +// Vertical button groups
    +// ----------------------
    +
    +.btn-group-vertical {
    +  > .btn,
    +  > .btn-group,
    +  > .btn-group > .btn {
    +    display: block;
    +    float: none;
    +    width: 100%;
    +    max-width: 100%;
    +  }
    +
    +  // Clear floats so dropdown menus can be properly placed
    +  > .btn-group {
    +    @include clearfix;
    +    > .btn {
    +      float: none;
    +    }
    +  }
    +
    +  > .btn + .btn,
    +  > .btn + .btn-group,
    +  > .btn-group + .btn,
    +  > .btn-group + .btn-group {
    +    margin-top: -1px;
    +    margin-left: 0;
    +  }
    +}
    +
    +.btn-group-vertical > .btn {
    +  &:not(:first-child):not(:last-child) {
    +    border-radius: 0;
    +  }
    +  &:first-child:not(:last-child) {
    +    @include border-top-radius($btn-border-radius-base);
    +    @include border-bottom-radius(0);
    +  }
    +  &:last-child:not(:first-child) {
    +    @include border-top-radius(0);
    +    @include border-bottom-radius($btn-border-radius-base);
    +  }
    +}
    +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
    +  border-radius: 0;
    +}
    +.btn-group-vertical > .btn-group:first-child:not(:last-child) {
    +  > .btn:last-child,
    +  > .dropdown-toggle {
    +    @include border-bottom-radius(0);
    +  }
    +}
    +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
    +  @include border-top-radius(0);
    +}
    +
    +
    +// Justified button groups
    +// ----------------------
    +
    +.btn-group-justified {
    +  display: table;
    +  width: 100%;
    +  table-layout: fixed;
    +  border-collapse: separate;
    +  > .btn,
    +  > .btn-group {
    +    float: none;
    +    display: table-cell;
    +    width: 1%;
    +  }
    +  > .btn-group .btn {
    +    width: 100%;
    +  }
    +
    +  > .btn-group .dropdown-menu {
    +    left: auto;
    +  }
    +}
    +
    +
    +// Checkbox and radio options
    +//
    +// In order to support the browser's form validation feedback, powered by the
    +// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
    +// `display: none;` or `visibility: hidden;` as that also hides the popover.
    +// Simply visually hiding the inputs via `opacity` would leave them clickable in
    +// certain cases which is prevented by using `clip` and `pointer-events`.
    +// This way, we ensure a DOM element is visible to position the popover from.
    +//
    +// See https://github.com/twbs/bootstrap/pull/12794 and
    +// https://github.com/twbs/bootstrap/pull/14559 for more information.
    +
    +[data-toggle="buttons"] {
    +  > .btn,
    +  > .btn-group > .btn {
    +    input[type="radio"],
    +    input[type="checkbox"] {
    +      position: absolute;
    +      clip: rect(0,0,0,0);
    +      pointer-events: none;
    +    }
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_buttons.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_buttons.scss
    new file mode 100644
    index 0000000..6452b70
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_buttons.scss
    @@ -0,0 +1,168 @@
    +//
    +// Buttons
    +// --------------------------------------------------
    +
    +
    +// Base styles
    +// --------------------------------------------------
    +
    +.btn {
    +  display: inline-block;
    +  margin-bottom: 0; // For input.btn
    +  font-weight: $btn-font-weight;
    +  text-align: center;
    +  vertical-align: middle;
    +  touch-action: manipulation;
    +  cursor: pointer;
    +  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
    +  border: 1px solid transparent;
    +  white-space: nowrap;
    +  @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base);
    +  @include user-select(none);
    +
    +  &,
    +  &:active,
    +  &.active {
    +    &:focus,
    +    &.focus {
    +      @include tab-focus;
    +    }
    +  }
    +
    +  &:hover,
    +  &:focus,
    +  &.focus {
    +    color: $btn-default-color;
    +    text-decoration: none;
    +  }
    +
    +  &:active,
    +  &.active {
    +    outline: 0;
    +    background-image: none;
    +    @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
    +  }
    +
    +  &.disabled,
    +  &[disabled],
    +  fieldset[disabled] & {
    +    cursor: $cursor-disabled;
    +    @include opacity(.65);
    +    @include box-shadow(none);
    +  }
    +
    +  // [converter] extracted a& to a.btn
    +}
    +
    +a.btn {
    +  &.disabled,
    +  fieldset[disabled] & {
    +    pointer-events: none; // Future-proof disabling of clicks on `` elements
    +  }
    +}
    +
    +
    +// Alternate buttons
    +// --------------------------------------------------
    +
    +.btn-default {
    +  @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border);
    +}
    +.btn-primary {
    +  @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border);
    +}
    +// Success appears as green
    +.btn-success {
    +  @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border);
    +}
    +// Info appears as blue-green
    +.btn-info {
    +  @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border);
    +}
    +// Warning appears as orange
    +.btn-warning {
    +  @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border);
    +}
    +// Danger and error appear as red
    +.btn-danger {
    +  @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border);
    +}
    +
    +
    +// Link buttons
    +// -------------------------
    +
    +// Make a button look and behave like a link
    +.btn-link {
    +  color: $link-color;
    +  font-weight: normal;
    +  border-radius: 0;
    +
    +  &,
    +  &:active,
    +  &.active,
    +  &[disabled],
    +  fieldset[disabled] & {
    +    background-color: transparent;
    +    @include box-shadow(none);
    +  }
    +  &,
    +  &:hover,
    +  &:focus,
    +  &:active {
    +    border-color: transparent;
    +  }
    +  &:hover,
    +  &:focus {
    +    color: $link-hover-color;
    +    text-decoration: $link-hover-decoration;
    +    background-color: transparent;
    +  }
    +  &[disabled],
    +  fieldset[disabled] & {
    +    &:hover,
    +    &:focus {
    +      color: $btn-link-disabled-color;
    +      text-decoration: none;
    +    }
    +  }
    +}
    +
    +
    +// Button Sizes
    +// --------------------------------------------------
    +
    +.btn-lg {
    +  // line-height: ensure even-numbered height of button next to large input
    +  @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $btn-border-radius-large);
    +}
    +.btn-sm {
    +  // line-height: ensure proper height of button next to small input
    +  @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small);
    +}
    +.btn-xs {
    +  @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small);
    +}
    +
    +
    +// Block button
    +// --------------------------------------------------
    +
    +.btn-block {
    +  display: block;
    +  width: 100%;
    +}
    +
    +// Vertically space out multiple block buttons
    +.btn-block + .btn-block {
    +  margin-top: 5px;
    +}
    +
    +// Specificity overrides
    +input[type="submit"],
    +input[type="reset"],
    +input[type="button"] {
    +  &.btn-block {
    +    width: 100%;
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_carousel.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_carousel.scss
    new file mode 100644
    index 0000000..753d881
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_carousel.scss
    @@ -0,0 +1,270 @@
    +//
    +// Carousel
    +// --------------------------------------------------
    +
    +
    +// Wrapper for the slide container and indicators
    +.carousel {
    +  position: relative;
    +}
    +
    +.carousel-inner {
    +  position: relative;
    +  overflow: hidden;
    +  width: 100%;
    +
    +  > .item {
    +    display: none;
    +    position: relative;
    +    @include transition(.6s ease-in-out left);
    +
    +    // Account for jankitude on images
    +    > img,
    +    > a > img {
    +      @include img-responsive;
    +      line-height: 1;
    +    }
    +
    +    // WebKit CSS3 transforms for supported devices
    +    @media all and (transform-3d), (-webkit-transform-3d) {
    +      @include transition-transform(0.6s ease-in-out);
    +      @include backface-visibility(hidden);
    +      @include perspective(1000px);
    +
    +      &.next,
    +      &.active.right {
    +        @include translate3d(100%, 0, 0);
    +        left: 0;
    +      }
    +      &.prev,
    +      &.active.left {
    +        @include translate3d(-100%, 0, 0);
    +        left: 0;
    +      }
    +      &.next.left,
    +      &.prev.right,
    +      &.active {
    +        @include translate3d(0, 0, 0);
    +        left: 0;
    +      }
    +    }
    +  }
    +
    +  > .active,
    +  > .next,
    +  > .prev {
    +    display: block;
    +  }
    +
    +  > .active {
    +    left: 0;
    +  }
    +
    +  > .next,
    +  > .prev {
    +    position: absolute;
    +    top: 0;
    +    width: 100%;
    +  }
    +
    +  > .next {
    +    left: 100%;
    +  }
    +  > .prev {
    +    left: -100%;
    +  }
    +  > .next.left,
    +  > .prev.right {
    +    left: 0;
    +  }
    +
    +  > .active.left {
    +    left: -100%;
    +  }
    +  > .active.right {
    +    left: 100%;
    +  }
    +
    +}
    +
    +// Left/right controls for nav
    +// ---------------------------
    +
    +.carousel-control {
    +  position: absolute;
    +  top: 0;
    +  left: 0;
    +  bottom: 0;
    +  width: $carousel-control-width;
    +  @include opacity($carousel-control-opacity);
    +  font-size: $carousel-control-font-size;
    +  color: $carousel-control-color;
    +  text-align: center;
    +  text-shadow: $carousel-text-shadow;
    +  background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug
    +  // We can't have this transition here because WebKit cancels the carousel
    +  // animation if you trip this while in the middle of another animation.
    +
    +  // Set gradients for backgrounds
    +  &.left {
    +    @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001));
    +  }
    +  &.right {
    +    left: auto;
    +    right: 0;
    +    @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5));
    +  }
    +
    +  // Hover/focus state
    +  &:hover,
    +  &:focus {
    +    outline: 0;
    +    color: $carousel-control-color;
    +    text-decoration: none;
    +    @include opacity(.9);
    +  }
    +
    +  // Toggles
    +  .icon-prev,
    +  .icon-next,
    +  .glyphicon-chevron-left,
    +  .glyphicon-chevron-right {
    +    position: absolute;
    +    top: 50%;
    +    margin-top: -10px;
    +    z-index: 5;
    +    display: inline-block;
    +  }
    +  .icon-prev,
    +  .glyphicon-chevron-left {
    +    left: 50%;
    +    margin-left: -10px;
    +  }
    +  .icon-next,
    +  .glyphicon-chevron-right {
    +    right: 50%;
    +    margin-right: -10px;
    +  }
    +  .icon-prev,
    +  .icon-next {
    +    width:  20px;
    +    height: 20px;
    +    line-height: 1;
    +    font-family: serif;
    +  }
    +
    +
    +  .icon-prev {
    +    &:before {
    +      content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)
    +    }
    +  }
    +  .icon-next {
    +    &:before {
    +      content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)
    +    }
    +  }
    +}
    +
    +// Optional indicator pips
    +//
    +// Add an unordered list with the following class and add a list item for each
    +// slide your carousel holds.
    +
    +.carousel-indicators {
    +  position: absolute;
    +  bottom: 10px;
    +  left: 50%;
    +  z-index: 15;
    +  width: 60%;
    +  margin-left: -30%;
    +  padding-left: 0;
    +  list-style: none;
    +  text-align: center;
    +
    +  li {
    +    display: inline-block;
    +    width:  10px;
    +    height: 10px;
    +    margin: 1px;
    +    text-indent: -999px;
    +    border: 1px solid $carousel-indicator-border-color;
    +    border-radius: 10px;
    +    cursor: pointer;
    +
    +    // IE8-9 hack for event handling
    +    //
    +    // Internet Explorer 8-9 does not support clicks on elements without a set
    +    // `background-color`. We cannot use `filter` since that's not viewed as a
    +    // background color by the browser. Thus, a hack is needed.
    +    // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer
    +    //
    +    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we
    +    // set alpha transparency for the best results possible.
    +    background-color: #000 \9; // IE8
    +    background-color: rgba(0,0,0,0); // IE9
    +  }
    +  .active {
    +    margin: 0;
    +    width:  12px;
    +    height: 12px;
    +    background-color: $carousel-indicator-active-bg;
    +  }
    +}
    +
    +// Optional captions
    +// -----------------------------
    +// Hidden by default for smaller viewports
    +.carousel-caption {
    +  position: absolute;
    +  left: 15%;
    +  right: 15%;
    +  bottom: 20px;
    +  z-index: 10;
    +  padding-top: 20px;
    +  padding-bottom: 20px;
    +  color: $carousel-caption-color;
    +  text-align: center;
    +  text-shadow: $carousel-text-shadow;
    +  & .btn {
    +    text-shadow: none; // No shadow for button elements in carousel-caption
    +  }
    +}
    +
    +
    +// Scale up controls for tablets and up
    +@media screen and (min-width: $screen-sm-min) {
    +
    +  // Scale up the controls a smidge
    +  .carousel-control {
    +    .glyphicon-chevron-left,
    +    .glyphicon-chevron-right,
    +    .icon-prev,
    +    .icon-next {
    +      width: ($carousel-control-font-size * 1.5);
    +      height: ($carousel-control-font-size * 1.5);
    +      margin-top: ($carousel-control-font-size / -2);
    +      font-size: ($carousel-control-font-size * 1.5);
    +    }
    +    .glyphicon-chevron-left,
    +    .icon-prev {
    +      margin-left: ($carousel-control-font-size / -2);
    +    }
    +    .glyphicon-chevron-right,
    +    .icon-next {
    +      margin-right: ($carousel-control-font-size / -2);
    +    }
    +  }
    +
    +  // Show and left align the captions
    +  .carousel-caption {
    +    left: 20%;
    +    right: 20%;
    +    padding-bottom: 30px;
    +  }
    +
    +  // Move up the indicators
    +  .carousel-indicators {
    +    bottom: 20px;
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_close.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_close.scss
    new file mode 100644
    index 0000000..3b74d8a
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_close.scss
    @@ -0,0 +1,36 @@
    +//
    +// Close icons
    +// --------------------------------------------------
    +
    +
    +.close {
    +  float: right;
    +  font-size: ($font-size-base * 1.5);
    +  font-weight: $close-font-weight;
    +  line-height: 1;
    +  color: $close-color;
    +  text-shadow: $close-text-shadow;
    +  @include opacity(.2);
    +
    +  &:hover,
    +  &:focus {
    +    color: $close-color;
    +    text-decoration: none;
    +    cursor: pointer;
    +    @include opacity(.5);
    +  }
    +
    +  // [converter] extracted button& to button.close
    +}
    +
    +// Additional properties for button version
    +// iOS requires the button element instead of an anchor tag.
    +// If you want the anchor version, it requires `href="#"`.
    +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
    +button.close {
    +  padding: 0;
    +  cursor: pointer;
    +  background: transparent;
    +  border: 0;
    +  -webkit-appearance: none;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_code.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_code.scss
    new file mode 100644
    index 0000000..caa5f06
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_code.scss
    @@ -0,0 +1,69 @@
    +//
    +// Code (inline and block)
    +// --------------------------------------------------
    +
    +
    +// Inline and block code styles
    +code,
    +kbd,
    +pre,
    +samp {
    +  font-family: $font-family-monospace;
    +}
    +
    +// Inline code
    +code {
    +  padding: 2px 4px;
    +  font-size: 90%;
    +  color: $code-color;
    +  background-color: $code-bg;
    +  border-radius: $border-radius-base;
    +}
    +
    +// User input typically entered via keyboard
    +kbd {
    +  padding: 2px 4px;
    +  font-size: 90%;
    +  color: $kbd-color;
    +  background-color: $kbd-bg;
    +  border-radius: $border-radius-small;
    +  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);
    +
    +  kbd {
    +    padding: 0;
    +    font-size: 100%;
    +    font-weight: bold;
    +    box-shadow: none;
    +  }
    +}
    +
    +// Blocks of code
    +pre {
    +  display: block;
    +  padding: (($line-height-computed - 1) / 2);
    +  margin: 0 0 ($line-height-computed / 2);
    +  font-size: ($font-size-base - 1); // 14px to 13px
    +  line-height: $line-height-base;
    +  word-break: break-all;
    +  word-wrap: break-word;
    +  color: $pre-color;
    +  background-color: $pre-bg;
    +  border: 1px solid $pre-border-color;
    +  border-radius: $border-radius-base;
    +
    +  // Account for some code outputs that place code tags in pre tags
    +  code {
    +    padding: 0;
    +    font-size: inherit;
    +    color: inherit;
    +    white-space: pre-wrap;
    +    background-color: transparent;
    +    border-radius: 0;
    +  }
    +}
    +
    +// Enable scrollable blocks of code
    +.pre-scrollable {
    +  max-height: $pre-scrollable-max-height;
    +  overflow-y: scroll;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_component-animations.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_component-animations.scss
    new file mode 100644
    index 0000000..ca3b43c
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_component-animations.scss
    @@ -0,0 +1,37 @@
    +//
    +// Component animations
    +// --------------------------------------------------
    +
    +// Heads up!
    +//
    +// We don't use the `.opacity()` mixin here since it causes a bug with text
    +// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552.
    +
    +.fade {
    +  opacity: 0;
    +  @include transition(opacity .15s linear);
    +  &.in {
    +    opacity: 1;
    +  }
    +}
    +
    +.collapse {
    +  display: none;
    +
    +  &.in      { display: block; }
    +  // [converter] extracted tr&.in to tr.collapse.in
    +  // [converter] extracted tbody&.in to tbody.collapse.in
    +}
    +
    +tr.collapse.in    { display: table-row; }
    +
    +tbody.collapse.in { display: table-row-group; }
    +
    +.collapsing {
    +  position: relative;
    +  height: 0;
    +  overflow: hidden;
    +  @include transition-property(height, visibility);
    +  @include transition-duration(.35s);
    +  @include transition-timing-function(ease);
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_dropdowns.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_dropdowns.scss
    new file mode 100644
    index 0000000..aac8459
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_dropdowns.scss
    @@ -0,0 +1,216 @@
    +//
    +// Dropdown menus
    +// --------------------------------------------------
    +
    +
    +// Dropdown arrow/caret
    +.caret {
    +  display: inline-block;
    +  width: 0;
    +  height: 0;
    +  margin-left: 2px;
    +  vertical-align: middle;
    +  border-top:   $caret-width-base dashed;
    +  border-top:   $caret-width-base solid \9; // IE8
    +  border-right: $caret-width-base solid transparent;
    +  border-left:  $caret-width-base solid transparent;
    +}
    +
    +// The dropdown wrapper (div)
    +.dropup,
    +.dropdown {
    +  position: relative;
    +}
    +
    +// Prevent the focus on the dropdown toggle when closing dropdowns
    +.dropdown-toggle:focus {
    +  outline: 0;
    +}
    +
    +// The dropdown menu (ul)
    +.dropdown-menu {
    +  position: absolute;
    +  top: 100%;
    +  left: 0;
    +  z-index: $zindex-dropdown;
    +  display: none; // none by default, but block on "open" of the menu
    +  float: left;
    +  min-width: 160px;
    +  padding: 5px 0;
    +  margin: 2px 0 0; // override default ul
    +  list-style: none;
    +  font-size: $font-size-base;
    +  text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
    +  background-color: $dropdown-bg;
    +  border: 1px solid $dropdown-fallback-border; // IE8 fallback
    +  border: 1px solid $dropdown-border;
    +  border-radius: $border-radius-base;
    +  @include box-shadow(0 6px 12px rgba(0,0,0,.175));
    +  background-clip: padding-box;
    +
    +  // Aligns the dropdown menu to right
    +  //
    +  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`
    +  &.pull-right {
    +    right: 0;
    +    left: auto;
    +  }
    +
    +  // Dividers (basically an hr) within the dropdown
    +  .divider {
    +    @include nav-divider($dropdown-divider-bg);
    +  }
    +
    +  // Links within the dropdown menu
    +  > li > a {
    +    display: block;
    +    padding: 3px 20px;
    +    clear: both;
    +    font-weight: normal;
    +    line-height: $line-height-base;
    +    color: $dropdown-link-color;
    +    white-space: nowrap; // prevent links from randomly breaking onto new lines
    +  }
    +}
    +
    +// Hover/Focus state
    +.dropdown-menu > li > a {
    +  &:hover,
    +  &:focus {
    +    text-decoration: none;
    +    color: $dropdown-link-hover-color;
    +    background-color: $dropdown-link-hover-bg;
    +  }
    +}
    +
    +// Active state
    +.dropdown-menu > .active > a {
    +  &,
    +  &:hover,
    +  &:focus {
    +    color: $dropdown-link-active-color;
    +    text-decoration: none;
    +    outline: 0;
    +    background-color: $dropdown-link-active-bg;
    +  }
    +}
    +
    +// Disabled state
    +//
    +// Gray out text and ensure the hover/focus state remains gray
    +
    +.dropdown-menu > .disabled > a {
    +  &,
    +  &:hover,
    +  &:focus {
    +    color: $dropdown-link-disabled-color;
    +  }
    +
    +  // Nuke hover/focus effects
    +  &:hover,
    +  &:focus {
    +    text-decoration: none;
    +    background-color: transparent;
    +    background-image: none; // Remove CSS gradient
    +    @include reset-filter;
    +    cursor: $cursor-disabled;
    +  }
    +}
    +
    +// Open state for the dropdown
    +.open {
    +  // Show the menu
    +  > .dropdown-menu {
    +    display: block;
    +  }
    +
    +  // Remove the outline when :focus is triggered
    +  > a {
    +    outline: 0;
    +  }
    +}
    +
    +// Menu positioning
    +//
    +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown
    +// menu with the parent.
    +.dropdown-menu-right {
    +  left: auto; // Reset the default from `.dropdown-menu`
    +  right: 0;
    +}
    +// With v3, we enabled auto-flipping if you have a dropdown within a right
    +// aligned nav component. To enable the undoing of that, we provide an override
    +// to restore the default dropdown menu alignment.
    +//
    +// This is only for left-aligning a dropdown menu within a `.navbar-right` or
    +// `.pull-right` nav component.
    +.dropdown-menu-left {
    +  left: 0;
    +  right: auto;
    +}
    +
    +// Dropdown section headers
    +.dropdown-header {
    +  display: block;
    +  padding: 3px 20px;
    +  font-size: $font-size-small;
    +  line-height: $line-height-base;
    +  color: $dropdown-header-color;
    +  white-space: nowrap; // as with > li > a
    +}
    +
    +// Backdrop to catch body clicks on mobile, etc.
    +.dropdown-backdrop {
    +  position: fixed;
    +  left: 0;
    +  right: 0;
    +  bottom: 0;
    +  top: 0;
    +  z-index: ($zindex-dropdown - 10);
    +}
    +
    +// Right aligned dropdowns
    +.pull-right > .dropdown-menu {
    +  right: 0;
    +  left: auto;
    +}
    +
    +// Allow for dropdowns to go bottom up (aka, dropup-menu)
    +//
    +// Just add .dropup after the standard .dropdown class and you're set, bro.
    +// TODO: abstract this so that the navbar fixed styles are not placed here?
    +
    +.dropup,
    +.navbar-fixed-bottom .dropdown {
    +  // Reverse the caret
    +  .caret {
    +    border-top: 0;
    +    border-bottom: $caret-width-base dashed;
    +    border-bottom: $caret-width-base solid \9; // IE8
    +    content: "";
    +  }
    +  // Different positioning for bottom up menu
    +  .dropdown-menu {
    +    top: auto;
    +    bottom: 100%;
    +    margin-bottom: 2px;
    +  }
    +}
    +
    +
    +// Component alignment
    +//
    +// Reiterate per navbar.less and the modified component alignment there.
    +
    +@media (min-width: $grid-float-breakpoint) {
    +  .navbar-right {
    +    .dropdown-menu {
    +      right: 0; left: auto;
    +    }
    +    // Necessary for overrides of the default right aligned menu.
    +    // Will remove come v4 in all likelihood.
    +    .dropdown-menu-left {
    +      left: 0; right: auto;
    +    }
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_forms.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_forms.scss
    new file mode 100644
    index 0000000..11ba109
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_forms.scss
    @@ -0,0 +1,617 @@
    +//
    +// Forms
    +// --------------------------------------------------
    +
    +
    +// Normalize non-controls
    +//
    +// Restyle and baseline non-control form elements.
    +
    +fieldset {
    +  padding: 0;
    +  margin: 0;
    +  border: 0;
    +  // Chrome and Firefox set a `min-width: min-content;` on fieldsets,
    +  // so we reset that to ensure it behaves more like a standard block element.
    +  // See https://github.com/twbs/bootstrap/issues/12359.
    +  min-width: 0;
    +}
    +
    +legend {
    +  display: block;
    +  width: 100%;
    +  padding: 0;
    +  margin-bottom: $line-height-computed;
    +  font-size: ($font-size-base * 1.5);
    +  line-height: inherit;
    +  color: $legend-color;
    +  border: 0;
    +  border-bottom: 1px solid $legend-border-color;
    +}
    +
    +label {
    +  display: inline-block;
    +  max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)
    +  margin-bottom: 5px;
    +  font-weight: bold;
    +}
    +
    +
    +// Normalize form controls
    +//
    +// While most of our form styles require extra classes, some basic normalization
    +// is required to ensure optimum display with or without those classes to better
    +// address browser inconsistencies.
    +
    +// Override content-box in Normalize (* isn't specific enough)
    +input[type="search"] {
    +  @include box-sizing(border-box);
    +}
    +
    +// Position radios and checkboxes better
    +input[type="radio"],
    +input[type="checkbox"] {
    +  margin: 4px 0 0;
    +  margin-top: 1px \9; // IE8-9
    +  line-height: normal;
    +}
    +
    +input[type="file"] {
    +  display: block;
    +}
    +
    +// Make range inputs behave like textual form controls
    +input[type="range"] {
    +  display: block;
    +  width: 100%;
    +}
    +
    +// Make multiple select elements height not fixed
    +select[multiple],
    +select[size] {
    +  height: auto;
    +}
    +
    +// Focus for file, radio, and checkbox
    +input[type="file"]:focus,
    +input[type="radio"]:focus,
    +input[type="checkbox"]:focus {
    +  @include tab-focus;
    +}
    +
    +// Adjust output element
    +output {
    +  display: block;
    +  padding-top: ($padding-base-vertical + 1);
    +  font-size: $font-size-base;
    +  line-height: $line-height-base;
    +  color: $input-color;
    +}
    +
    +
    +// Common form controls
    +//
    +// Shared size and type resets for form controls. Apply `.form-control` to any
    +// of the following form controls:
    +//
    +// select
    +// textarea
    +// input[type="text"]
    +// input[type="password"]
    +// input[type="datetime"]
    +// input[type="datetime-local"]
    +// input[type="date"]
    +// input[type="month"]
    +// input[type="time"]
    +// input[type="week"]
    +// input[type="number"]
    +// input[type="email"]
    +// input[type="url"]
    +// input[type="search"]
    +// input[type="tel"]
    +// input[type="color"]
    +
    +.form-control {
    +  display: block;
    +  width: 100%;
    +  height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)
    +  padding: $padding-base-vertical $padding-base-horizontal;
    +  font-size: $font-size-base;
    +  line-height: $line-height-base;
    +  color: $input-color;
    +  background-color: $input-bg;
    +  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
    +  border: 1px solid $input-border;
    +  border-radius: $input-border-radius; // Note: This has no effect on s in CSS.
    +  @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
    +  @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
    +
    +  // Customize the `:focus` state to imitate native WebKit styles.
    +  @include form-control-focus;
    +
    +  // Placeholder
    +  @include placeholder;
    +
    +  // Unstyle the caret on `` background color
    +$input-bg:                       #fff !default;
    +//** `` background color
    +$input-bg-disabled:              $gray-lighter !default;
    +
    +//** Text color for ``s
    +$input-color:                    $gray !default;
    +//** `` border color
    +$input-border:                   #ccc !default;
    +
    +// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
    +//** Default `.form-control` border radius
    +// This has no effect on ``s in CSS.
    +$input-border-radius:            $border-radius-base !default;
    +//** Large `.form-control` border radius
    +$input-border-radius-large:      $border-radius-large !default;
    +//** Small `.form-control` border radius
    +$input-border-radius-small:      $border-radius-small !default;
    +
    +//** Border color for inputs on focus
    +$input-border-focus:             #66afe9 !default;
    +
    +//** Placeholder text color
    +$input-color-placeholder:        #999 !default;
    +
    +//** Default `.form-control` height
    +$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2) !default;
    +//** Large `.form-control` height
    +$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default;
    +//** Small `.form-control` height
    +$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default;
    +
    +//** `.form-group` margin
    +$form-group-margin-bottom:       15px !default;
    +
    +$legend-color:                   $gray-dark !default;
    +$legend-border-color:            #e5e5e5 !default;
    +
    +//** Background color for textual input addons
    +$input-group-addon-bg:           $gray-lighter !default;
    +//** Border color for textual input addons
    +$input-group-addon-border-color: $input-border !default;
    +
    +//** Disabled cursor for form controls and buttons.
    +$cursor-disabled:                not-allowed !default;
    +
    +
    +//== Dropdowns
    +//
    +//## Dropdown menu container and contents.
    +
    +//** Background for the dropdown menu.
    +$dropdown-bg:                    #fff !default;
    +//** Dropdown menu `border-color`.
    +$dropdown-border:                rgba(0,0,0,.15) !default;
    +//** Dropdown menu `border-color` **for IE8**.
    +$dropdown-fallback-border:       #ccc !default;
    +//** Divider color for between dropdown items.
    +$dropdown-divider-bg:            #e5e5e5 !default;
    +
    +//** Dropdown link text color.
    +$dropdown-link-color:            $gray-dark !default;
    +//** Hover color for dropdown links.
    +$dropdown-link-hover-color:      darken($gray-dark, 5%) !default;
    +//** Hover background for dropdown links.
    +$dropdown-link-hover-bg:         #f5f5f5 !default;
    +
    +//** Active dropdown menu item text color.
    +$dropdown-link-active-color:     $component-active-color !default;
    +//** Active dropdown menu item background color.
    +$dropdown-link-active-bg:        $component-active-bg !default;
    +
    +//** Disabled dropdown menu item background color.
    +$dropdown-link-disabled-color:   $gray-light !default;
    +
    +//** Text color for headers within dropdown menus.
    +$dropdown-header-color:          $gray-light !default;
    +
    +//** Deprecated `$dropdown-caret-color` as of v3.1.0
    +$dropdown-caret-color:           #000 !default;
    +
    +
    +//-- Z-index master list
    +//
    +// Warning: Avoid customizing these values. They're used for a bird's eye view
    +// of components dependent on the z-axis and are designed to all work together.
    +//
    +// Note: These variables are not generated into the Customizer.
    +
    +$zindex-navbar:            1000 !default;
    +$zindex-dropdown:          1000 !default;
    +$zindex-popover:           1060 !default;
    +$zindex-tooltip:           1070 !default;
    +$zindex-navbar-fixed:      1030 !default;
    +$zindex-modal-background:  1040 !default;
    +$zindex-modal:             1050 !default;
    +
    +
    +//== Media queries breakpoints
    +//
    +//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
    +
    +// Extra small screen / phone
    +//** Deprecated `$screen-xs` as of v3.0.1
    +$screen-xs:                  480px !default;
    +//** Deprecated `$screen-xs-min` as of v3.2.0
    +$screen-xs-min:              $screen-xs !default;
    +//** Deprecated `$screen-phone` as of v3.0.1
    +$screen-phone:               $screen-xs-min !default;
    +
    +// Small screen / tablet
    +//** Deprecated `$screen-sm` as of v3.0.1
    +$screen-sm:                  768px !default;
    +$screen-sm-min:              $screen-sm !default;
    +//** Deprecated `$screen-tablet` as of v3.0.1
    +$screen-tablet:              $screen-sm-min !default;
    +
    +// Medium screen / desktop
    +//** Deprecated `$screen-md` as of v3.0.1
    +$screen-md:                  992px !default;
    +$screen-md-min:              $screen-md !default;
    +//** Deprecated `$screen-desktop` as of v3.0.1
    +$screen-desktop:             $screen-md-min !default;
    +
    +// Large screen / wide desktop
    +//** Deprecated `$screen-lg` as of v3.0.1
    +$screen-lg:                  1200px !default;
    +$screen-lg-min:              $screen-lg !default;
    +//** Deprecated `$screen-lg-desktop` as of v3.0.1
    +$screen-lg-desktop:          $screen-lg-min !default;
    +
    +// So media queries don't overlap when required, provide a maximum
    +$screen-xs-max:              ($screen-sm-min - 1) !default;
    +$screen-sm-max:              ($screen-md-min - 1) !default;
    +$screen-md-max:              ($screen-lg-min - 1) !default;
    +
    +
    +//== Grid system
    +//
    +//## Define your custom responsive grid.
    +
    +//** Number of columns in the grid.
    +$grid-columns:              12 !default;
    +//** Padding between columns. Gets divided in half for the left and right.
    +$grid-gutter-width:         30px !default;
    +// Navbar collapse
    +//** Point at which the navbar becomes uncollapsed.
    +$grid-float-breakpoint:     $screen-sm-min !default;
    +//** Point at which the navbar begins collapsing.
    +$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default;
    +
    +
    +//== Container sizes
    +//
    +//## Define the maximum width of `.container` for different screen sizes.
    +
    +// Small screen / tablet
    +$container-tablet:             (720px + $grid-gutter-width) !default;
    +//** For `$screen-sm-min` and up.
    +$container-sm:                 $container-tablet !default;
    +
    +// Medium screen / desktop
    +$container-desktop:            (940px + $grid-gutter-width) !default;
    +//** For `$screen-md-min` and up.
    +$container-md:                 $container-desktop !default;
    +
    +// Large screen / wide desktop
    +$container-large-desktop:      (1140px + $grid-gutter-width) !default;
    +//** For `$screen-lg-min` and up.
    +$container-lg:                 $container-large-desktop !default;
    +
    +
    +//== Navbar
    +//
    +//##
    +
    +// Basics of a navbar
    +$navbar-height:                    50px !default;
    +$navbar-margin-bottom:             $line-height-computed !default;
    +$navbar-border-radius:             $border-radius-base !default;
    +$navbar-padding-horizontal:        floor(($grid-gutter-width / 2)) !default;
    +$navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2) !default;
    +$navbar-collapse-max-height:       340px !default;
    +
    +$navbar-default-color:             #777 !default;
    +$navbar-default-bg:                #f8f8f8 !default;
    +$navbar-default-border:            darken($navbar-default-bg, 6.5%) !default;
    +
    +// Navbar links
    +$navbar-default-link-color:                #777 !default;
    +$navbar-default-link-hover-color:          #333 !default;
    +$navbar-default-link-hover-bg:             transparent !default;
    +$navbar-default-link-active-color:         #555 !default;
    +$navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%) !default;
    +$navbar-default-link-disabled-color:       #ccc !default;
    +$navbar-default-link-disabled-bg:          transparent !default;
    +
    +// Navbar brand label
    +$navbar-default-brand-color:               $navbar-default-link-color !default;
    +$navbar-default-brand-hover-color:         darken($navbar-default-brand-color, 10%) !default;
    +$navbar-default-brand-hover-bg:            transparent !default;
    +
    +// Navbar toggle
    +$navbar-default-toggle-hover-bg:           #ddd !default;
    +$navbar-default-toggle-icon-bar-bg:        #888 !default;
    +$navbar-default-toggle-border-color:       #ddd !default;
    +
    +
    +//=== Inverted navbar
    +// Reset inverted navbar basics
    +$navbar-inverse-color:                      lighten($gray-light, 15%) !default;
    +$navbar-inverse-bg:                         #222 !default;
    +$navbar-inverse-border:                     darken($navbar-inverse-bg, 10%) !default;
    +
    +// Inverted navbar links
    +$navbar-inverse-link-color:                 lighten($gray-light, 15%) !default;
    +$navbar-inverse-link-hover-color:           #fff !default;
    +$navbar-inverse-link-hover-bg:              transparent !default;
    +$navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color !default;
    +$navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%) !default;
    +$navbar-inverse-link-disabled-color:        #444 !default;
    +$navbar-inverse-link-disabled-bg:           transparent !default;
    +
    +// Inverted navbar brand label
    +$navbar-inverse-brand-color:                $navbar-inverse-link-color !default;
    +$navbar-inverse-brand-hover-color:          #fff !default;
    +$navbar-inverse-brand-hover-bg:             transparent !default;
    +
    +// Inverted navbar toggle
    +$navbar-inverse-toggle-hover-bg:            #333 !default;
    +$navbar-inverse-toggle-icon-bar-bg:         #fff !default;
    +$navbar-inverse-toggle-border-color:        #333 !default;
    +
    +
    +//== Navs
    +//
    +//##
    +
    +//=== Shared nav styles
    +$nav-link-padding:                          10px 15px !default;
    +$nav-link-hover-bg:                         $gray-lighter !default;
    +
    +$nav-disabled-link-color:                   $gray-light !default;
    +$nav-disabled-link-hover-color:             $gray-light !default;
    +
    +//== Tabs
    +$nav-tabs-border-color:                     #ddd !default;
    +
    +$nav-tabs-link-hover-border-color:          $gray-lighter !default;
    +
    +$nav-tabs-active-link-hover-bg:             $body-bg !default;
    +$nav-tabs-active-link-hover-color:          $gray !default;
    +$nav-tabs-active-link-hover-border-color:   #ddd !default;
    +
    +$nav-tabs-justified-link-border-color:            #ddd !default;
    +$nav-tabs-justified-active-link-border-color:     $body-bg !default;
    +
    +//== Pills
    +$nav-pills-border-radius:                   $border-radius-base !default;
    +$nav-pills-active-link-hover-bg:            $component-active-bg !default;
    +$nav-pills-active-link-hover-color:         $component-active-color !default;
    +
    +
    +//== Pagination
    +//
    +//##
    +
    +$pagination-color:                     $link-color !default;
    +$pagination-bg:                        #fff !default;
    +$pagination-border:                    #ddd !default;
    +
    +$pagination-hover-color:               $link-hover-color !default;
    +$pagination-hover-bg:                  $gray-lighter !default;
    +$pagination-hover-border:              #ddd !default;
    +
    +$pagination-active-color:              #fff !default;
    +$pagination-active-bg:                 $brand-primary !default;
    +$pagination-active-border:             $brand-primary !default;
    +
    +$pagination-disabled-color:            $gray-light !default;
    +$pagination-disabled-bg:               #fff !default;
    +$pagination-disabled-border:           #ddd !default;
    +
    +
    +//== Pager
    +//
    +//##
    +
    +$pager-bg:                             $pagination-bg !default;
    +$pager-border:                         $pagination-border !default;
    +$pager-border-radius:                  15px !default;
    +
    +$pager-hover-bg:                       $pagination-hover-bg !default;
    +
    +$pager-active-bg:                      $pagination-active-bg !default;
    +$pager-active-color:                   $pagination-active-color !default;
    +
    +$pager-disabled-color:                 $pagination-disabled-color !default;
    +
    +
    +//== Jumbotron
    +//
    +//##
    +
    +$jumbotron-padding:              30px !default;
    +$jumbotron-color:                inherit !default;
    +$jumbotron-bg:                   $gray-lighter !default;
    +$jumbotron-heading-color:        inherit !default;
    +$jumbotron-font-size:            ceil(($font-size-base * 1.5)) !default;
    +$jumbotron-heading-font-size:    ceil(($font-size-base * 4.5)) !default;
    +
    +
    +//== Form states and alerts
    +//
    +//## Define colors for form feedback states and, by default, alerts.
    +
    +$state-success-text:             #3c763d !default;
    +$state-success-bg:               #dff0d8 !default;
    +$state-success-border:           darken(adjust-hue($state-success-bg, -10), 5%) !default;
    +
    +$state-info-text:                #31708f !default;
    +$state-info-bg:                  #d9edf7 !default;
    +$state-info-border:              darken(adjust-hue($state-info-bg, -10), 7%) !default;
    +
    +$state-warning-text:             #8a6d3b !default;
    +$state-warning-bg:               #fcf8e3 !default;
    +$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 5%) !default;
    +
    +$state-danger-text:              #a94442 !default;
    +$state-danger-bg:                #f2dede !default;
    +$state-danger-border:            darken(adjust-hue($state-danger-bg, -10), 5%) !default;
    +
    +
    +//== Tooltips
    +//
    +//##
    +
    +//** Tooltip max width
    +$tooltip-max-width:           200px !default;
    +//** Tooltip text color
    +$tooltip-color:               #fff !default;
    +//** Tooltip background color
    +$tooltip-bg:                  #000 !default;
    +$tooltip-opacity:             .9 !default;
    +
    +//** Tooltip arrow width
    +$tooltip-arrow-width:         5px !default;
    +//** Tooltip arrow color
    +$tooltip-arrow-color:         $tooltip-bg !default;
    +
    +
    +//== Popovers
    +//
    +//##
    +
    +//** Popover body background color
    +$popover-bg:                          #fff !default;
    +//** Popover maximum width
    +$popover-max-width:                   276px !default;
    +//** Popover border color
    +$popover-border-color:                rgba(0,0,0,.2) !default;
    +//** Popover fallback border color
    +$popover-fallback-border-color:       #ccc !default;
    +
    +//** Popover title background color
    +$popover-title-bg:                    darken($popover-bg, 3%) !default;
    +
    +//** Popover arrow width
    +$popover-arrow-width:                 10px !default;
    +//** Popover arrow color
    +$popover-arrow-color:                 $popover-bg !default;
    +
    +//** Popover outer arrow width
    +$popover-arrow-outer-width:           ($popover-arrow-width + 1) !default;
    +//** Popover outer arrow color
    +$popover-arrow-outer-color:           fade_in($popover-border-color, 0.05) !default;
    +//** Popover outer arrow fallback color
    +$popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%) !default;
    +
    +
    +//== Labels
    +//
    +//##
    +
    +//** Default label background color
    +$label-default-bg:            $gray-light !default;
    +//** Primary label background color
    +$label-primary-bg:            $brand-primary !default;
    +//** Success label background color
    +$label-success-bg:            $brand-success !default;
    +//** Info label background color
    +$label-info-bg:               $brand-info !default;
    +//** Warning label background color
    +$label-warning-bg:            $brand-warning !default;
    +//** Danger label background color
    +$label-danger-bg:             $brand-danger !default;
    +
    +//** Default label text color
    +$label-color:                 #fff !default;
    +//** Default text color of a linked label
    +$label-link-hover-color:      #fff !default;
    +
    +
    +//== Modals
    +//
    +//##
    +
    +//** Padding applied to the modal body
    +$modal-inner-padding:         15px !default;
    +
    +//** Padding applied to the modal title
    +$modal-title-padding:         15px !default;
    +//** Modal title line-height
    +$modal-title-line-height:     $line-height-base !default;
    +
    +//** Background color of modal content area
    +$modal-content-bg:                             #fff !default;
    +//** Modal content border color
    +$modal-content-border-color:                   rgba(0,0,0,.2) !default;
    +//** Modal content border color **for IE8**
    +$modal-content-fallback-border-color:          #999 !default;
    +
    +//** Modal backdrop background color
    +$modal-backdrop-bg:           #000 !default;
    +//** Modal backdrop opacity
    +$modal-backdrop-opacity:      .5 !default;
    +//** Modal header border color
    +$modal-header-border-color:   #e5e5e5 !default;
    +//** Modal footer border color
    +$modal-footer-border-color:   $modal-header-border-color !default;
    +
    +$modal-lg:                    900px !default;
    +$modal-md:                    600px !default;
    +$modal-sm:                    300px !default;
    +
    +
    +//== Alerts
    +//
    +//## Define alert colors, border radius, and padding.
    +
    +$alert-padding:               15px !default;
    +$alert-border-radius:         $border-radius-base !default;
    +$alert-link-font-weight:      bold !default;
    +
    +$alert-success-bg:            $state-success-bg !default;
    +$alert-success-text:          $state-success-text !default;
    +$alert-success-border:        $state-success-border !default;
    +
    +$alert-info-bg:               $state-info-bg !default;
    +$alert-info-text:             $state-info-text !default;
    +$alert-info-border:           $state-info-border !default;
    +
    +$alert-warning-bg:            $state-warning-bg !default;
    +$alert-warning-text:          $state-warning-text !default;
    +$alert-warning-border:        $state-warning-border !default;
    +
    +$alert-danger-bg:             $state-danger-bg !default;
    +$alert-danger-text:           $state-danger-text !default;
    +$alert-danger-border:         $state-danger-border !default;
    +
    +
    +//== Progress bars
    +//
    +//##
    +
    +//** Background color of the whole progress component
    +$progress-bg:                 #f5f5f5 !default;
    +//** Progress bar text color
    +$progress-bar-color:          #fff !default;
    +//** Variable for setting rounded corners on progress bar.
    +$progress-border-radius:      $border-radius-base !default;
    +
    +//** Default progress bar color
    +$progress-bar-bg:             $brand-primary !default;
    +//** Success progress bar color
    +$progress-bar-success-bg:     $brand-success !default;
    +//** Warning progress bar color
    +$progress-bar-warning-bg:     $brand-warning !default;
    +//** Danger progress bar color
    +$progress-bar-danger-bg:      $brand-danger !default;
    +//** Info progress bar color
    +$progress-bar-info-bg:        $brand-info !default;
    +
    +
    +//== List group
    +//
    +//##
    +
    +//** Background color on `.list-group-item`
    +$list-group-bg:                 #fff !default;
    +//** `.list-group-item` border color
    +$list-group-border:             #ddd !default;
    +//** List group border radius
    +$list-group-border-radius:      $border-radius-base !default;
    +
    +//** Background color of single list items on hover
    +$list-group-hover-bg:           #f5f5f5 !default;
    +//** Text color of active list items
    +$list-group-active-color:       $component-active-color !default;
    +//** Background color of active list items
    +$list-group-active-bg:          $component-active-bg !default;
    +//** Border color of active list elements
    +$list-group-active-border:      $list-group-active-bg !default;
    +//** Text color for content within active list items
    +$list-group-active-text-color:  lighten($list-group-active-bg, 40%) !default;
    +
    +//** Text color of disabled list items
    +$list-group-disabled-color:      $gray-light !default;
    +//** Background color of disabled list items
    +$list-group-disabled-bg:         $gray-lighter !default;
    +//** Text color for content within disabled list items
    +$list-group-disabled-text-color: $list-group-disabled-color !default;
    +
    +$list-group-link-color:         #555 !default;
    +$list-group-link-hover-color:   $list-group-link-color !default;
    +$list-group-link-heading-color: #333 !default;
    +
    +
    +//== Panels
    +//
    +//##
    +
    +$panel-bg:                    #fff !default;
    +$panel-body-padding:          15px !default;
    +$panel-heading-padding:       10px 15px !default;
    +$panel-footer-padding:        $panel-heading-padding !default;
    +$panel-border-radius:         $border-radius-base !default;
    +
    +//** Border color for elements within panels
    +$panel-inner-border:          #ddd !default;
    +$panel-footer-bg:             #f5f5f5 !default;
    +
    +$panel-default-text:          $gray-dark !default;
    +$panel-default-border:        #ddd !default;
    +$panel-default-heading-bg:    #f5f5f5 !default;
    +
    +$panel-primary-text:          #fff !default;
    +$panel-primary-border:        $brand-primary !default;
    +$panel-primary-heading-bg:    $brand-primary !default;
    +
    +$panel-success-text:          $state-success-text !default;
    +$panel-success-border:        $state-success-border !default;
    +$panel-success-heading-bg:    $state-success-bg !default;
    +
    +$panel-info-text:             $state-info-text !default;
    +$panel-info-border:           $state-info-border !default;
    +$panel-info-heading-bg:       $state-info-bg !default;
    +
    +$panel-warning-text:          $state-warning-text !default;
    +$panel-warning-border:        $state-warning-border !default;
    +$panel-warning-heading-bg:    $state-warning-bg !default;
    +
    +$panel-danger-text:           $state-danger-text !default;
    +$panel-danger-border:         $state-danger-border !default;
    +$panel-danger-heading-bg:     $state-danger-bg !default;
    +
    +
    +//== Thumbnails
    +//
    +//##
    +
    +//** Padding around the thumbnail image
    +$thumbnail-padding:           4px !default;
    +//** Thumbnail background color
    +$thumbnail-bg:                $body-bg !default;
    +//** Thumbnail border color
    +$thumbnail-border:            #ddd !default;
    +//** Thumbnail border radius
    +$thumbnail-border-radius:     $border-radius-base !default;
    +
    +//** Custom text color for thumbnail captions
    +$thumbnail-caption-color:     $text-color !default;
    +//** Padding around the thumbnail caption
    +$thumbnail-caption-padding:   9px !default;
    +
    +
    +//== Wells
    +//
    +//##
    +
    +$well-bg:                     #f5f5f5 !default;
    +$well-border:                 darken($well-bg, 7%) !default;
    +
    +
    +//== Badges
    +//
    +//##
    +
    +$badge-color:                 #fff !default;
    +//** Linked badge text color on hover
    +$badge-link-hover-color:      #fff !default;
    +$badge-bg:                    $gray-light !default;
    +
    +//** Badge text color in active nav link
    +$badge-active-color:          $link-color !default;
    +//** Badge background color in active nav link
    +$badge-active-bg:             #fff !default;
    +
    +$badge-font-weight:           bold !default;
    +$badge-line-height:           1 !default;
    +$badge-border-radius:         10px !default;
    +
    +
    +//== Breadcrumbs
    +//
    +//##
    +
    +$breadcrumb-padding-vertical:   8px !default;
    +$breadcrumb-padding-horizontal: 15px !default;
    +//** Breadcrumb background color
    +$breadcrumb-bg:                 #f5f5f5 !default;
    +//** Breadcrumb text color
    +$breadcrumb-color:              #ccc !default;
    +//** Text color of current page in the breadcrumb
    +$breadcrumb-active-color:       $gray-light !default;
    +//** Textual separator for between breadcrumb elements
    +$breadcrumb-separator:          "/" !default;
    +
    +
    +//== Carousel
    +//
    +//##
    +
    +$carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6) !default;
    +
    +$carousel-control-color:                      #fff !default;
    +$carousel-control-width:                      15% !default;
    +$carousel-control-opacity:                    .5 !default;
    +$carousel-control-font-size:                  20px !default;
    +
    +$carousel-indicator-active-bg:                #fff !default;
    +$carousel-indicator-border-color:             #fff !default;
    +
    +$carousel-caption-color:                      #fff !default;
    +
    +
    +//== Close
    +//
    +//##
    +
    +$close-font-weight:           bold !default;
    +$close-color:                 #000 !default;
    +$close-text-shadow:           0 1px 0 #fff !default;
    +
    +
    +//== Code
    +//
    +//##
    +
    +$code-color:                  #c7254e !default;
    +$code-bg:                     #f9f2f4 !default;
    +
    +$kbd-color:                   #fff !default;
    +$kbd-bg:                      #333 !default;
    +
    +$pre-bg:                      #f5f5f5 !default;
    +$pre-color:                   $gray-dark !default;
    +$pre-border-color:            #ccc !default;
    +$pre-scrollable-max-height:   340px !default;
    +
    +
    +//== Type
    +//
    +//##
    +
    +//** Horizontal offset for forms and lists.
    +$component-offset-horizontal: 180px !default;
    +//** Text muted color
    +$text-muted:                  $gray-light !default;
    +//** Abbreviations and acronyms border color
    +$abbr-border-color:           $gray-light !default;
    +//** Headings small color
    +$headings-small-color:        $gray-light !default;
    +//** Blockquote small color
    +$blockquote-small-color:      $gray-light !default;
    +//** Blockquote font size
    +$blockquote-font-size:        ($font-size-base * 1.25) !default;
    +//** Blockquote border color
    +$blockquote-border-color:     $gray-lighter !default;
    +//** Page header border color
    +$page-header-border-color:    $gray-lighter !default;
    +//** Width of horizontal description list titles
    +$dl-horizontal-offset:        $component-offset-horizontal !default;
    +//** Point at which .dl-horizontal becomes horizontal
    +$dl-horizontal-breakpoint:    $grid-float-breakpoint !default;
    +//** Horizontal line color.
    +$hr-border:                   $gray-lighter !default;
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/_wells.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/_wells.scss
    new file mode 100644
    index 0000000..b865711
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/_wells.scss
    @@ -0,0 +1,29 @@
    +//
    +// Wells
    +// --------------------------------------------------
    +
    +
    +// Base class
    +.well {
    +  min-height: 20px;
    +  padding: 19px;
    +  margin-bottom: 20px;
    +  background-color: $well-bg;
    +  border: 1px solid $well-border;
    +  border-radius: $border-radius-base;
    +  @include box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
    +  blockquote {
    +    border-color: #ddd;
    +    border-color: rgba(0,0,0,.15);
    +  }
    +}
    +
    +// Sizes
    +.well-lg {
    +  padding: 24px;
    +  border-radius: $border-radius-large;
    +}
    +.well-sm {
    +  padding: 9px;
    +  border-radius: $border-radius-small;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_alerts.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_alerts.scss
    new file mode 100644
    index 0000000..3faf0b5
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_alerts.scss
    @@ -0,0 +1,14 @@
    +// Alerts
    +
    +@mixin alert-variant($background, $border, $text-color) {
    +  background-color: $background;
    +  border-color: $border;
    +  color: $text-color;
    +
    +  hr {
    +    border-top-color: darken($border, 5%);
    +  }
    +  .alert-link {
    +    color: darken($text-color, 10%);
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_background-variant.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_background-variant.scss
    new file mode 100644
    index 0000000..4c7769e
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_background-variant.scss
    @@ -0,0 +1,12 @@
    +// Contextual backgrounds
    +
    +// [converter] $parent hack
    +@mixin bg-variant($parent, $color) {
    +  #{$parent} {
    +    background-color: $color;
    +  }
    +  a#{$parent}:hover,
    +  a#{$parent}:focus {
    +    background-color: darken($color, 10%);
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_border-radius.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_border-radius.scss
    new file mode 100644
    index 0000000..ce19499
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_border-radius.scss
    @@ -0,0 +1,18 @@
    +// Single side border-radius
    +
    +@mixin border-top-radius($radius) {
    +  border-top-right-radius: $radius;
    +   border-top-left-radius: $radius;
    +}
    +@mixin border-right-radius($radius) {
    +  border-bottom-right-radius: $radius;
    +     border-top-right-radius: $radius;
    +}
    +@mixin border-bottom-radius($radius) {
    +  border-bottom-right-radius: $radius;
    +   border-bottom-left-radius: $radius;
    +}
    +@mixin border-left-radius($radius) {
    +  border-bottom-left-radius: $radius;
    +     border-top-left-radius: $radius;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_buttons.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_buttons.scss
    new file mode 100644
    index 0000000..b93f84b
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_buttons.scss
    @@ -0,0 +1,65 @@
    +// Button variants
    +//
    +// Easily pump out default styles, as well as :hover, :focus, :active,
    +// and disabled options for all buttons
    +
    +@mixin button-variant($color, $background, $border) {
    +  color: $color;
    +  background-color: $background;
    +  border-color: $border;
    +
    +  &:focus,
    +  &.focus {
    +    color: $color;
    +    background-color: darken($background, 10%);
    +        border-color: darken($border, 25%);
    +  }
    +  &:hover {
    +    color: $color;
    +    background-color: darken($background, 10%);
    +        border-color: darken($border, 12%);
    +  }
    +  &:active,
    +  &.active,
    +  .open > &.dropdown-toggle {
    +    color: $color;
    +    background-color: darken($background, 10%);
    +        border-color: darken($border, 12%);
    +
    +    &:hover,
    +    &:focus,
    +    &.focus {
    +      color: $color;
    +      background-color: darken($background, 17%);
    +          border-color: darken($border, 25%);
    +    }
    +  }
    +  &:active,
    +  &.active,
    +  .open > &.dropdown-toggle {
    +    background-image: none;
    +  }
    +  &.disabled,
    +  &[disabled],
    +  fieldset[disabled] & {
    +    &:hover,
    +    &:focus,
    +    &.focus {
    +      background-color: $background;
    +          border-color: $border;
    +    }
    +  }
    +
    +  .badge {
    +    color: $background;
    +    background-color: $color;
    +  }
    +}
    +
    +// Button sizes
    +@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
    +  padding: $padding-vertical $padding-horizontal;
    +  font-size: $font-size;
    +  line-height: $line-height;
    +  border-radius: $border-radius;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_center-block.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_center-block.scss
    new file mode 100644
    index 0000000..e06fb5e
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_center-block.scss
    @@ -0,0 +1,7 @@
    +// Center-align a block level element
    +
    +@mixin center-block() {
    +  display: block;
    +  margin-left: auto;
    +  margin-right: auto;
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_clearfix.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_clearfix.scss
    new file mode 100644
    index 0000000..dc3e2ab
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_clearfix.scss
    @@ -0,0 +1,22 @@
    +// Clearfix
    +//
    +// For modern browsers
    +// 1. The space content is one way to avoid an Opera bug when the
    +//    contenteditable attribute is included anywhere else in the document.
    +//    Otherwise it causes space to appear at the top and bottom of elements
    +//    that are clearfixed.
    +// 2. The use of `table` rather than `block` is only necessary if using
    +//    `:before` to contain the top-margins of child elements.
    +//
    +// Source: http://nicolasgallagher.com/micro-clearfix-hack/
    +
    +@mixin clearfix() {
    +  &:before,
    +  &:after {
    +    content: " "; // 1
    +    display: table; // 2
    +  }
    +  &:after {
    +    clear: both;
    +  }
    +}
    diff --git a/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_forms.scss b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_forms.scss
    new file mode 100644
    index 0000000..277aa5f
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap3/scss/bootstrap/mixins/_forms.scss
    @@ -0,0 +1,88 @@
    +// Form validation states
    +//
    +// Used in forms.less to generate the form validation CSS for warnings, errors,
    +// and successes.
    +
    +@mixin form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) {
    +  // Color the label and help text
    +  .help-block,
    +  .control-label,
    +  .radio,
    +  .checkbox,
    +  .radio-inline,
    +  .checkbox-inline,
    +  &.radio label,
    +  &.checkbox label,
    +  &.radio-inline label,
    +  &.checkbox-inline label  {
    +    color: $text-color;
    +  }
    +  // Set the border and box shadow on specific inputs to match
    +  .form-control {
    +    border-color: $border-color;
    +    @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work
    +    &:focus {
    +      border-color: darken($border-color, 10%);
    +      $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%);
    +      @include box-shadow($shadow);
    +    }
    +  }
    +  // Set validation states also for addons
    +  .input-group-addon {
    +    color: $text-color;
    +    border-color: $border-color;
    +    background-color: $background-color;
    +  }
    +  // Optional feedback icon
    +  .form-control-feedback {
    +    color: $text-color;
    +  }
    +}
    +
    +
    +// Form control focus state
    +//
    +// Generate a customized focus state and for any input with the specified color,
    +// which defaults to the `$input-border-focus` variable.
    +//
    +// We highly encourage you to not customize the default value, but instead use
    +// this to tweak colors on an as-needed basis. This aesthetic change is based on
    +// WebKit's default styles, but applicable to a wider range of browsers. Its
    +// usability and accessibility should be taken into account with any change.
    +//
    +// Example usage: change the default blue border and shadow to white for better
    +// contrast against a dark gray background.
    +@mixin form-control-focus($color: $input-border-focus) {
    +  $color-rgba: rgba(red($color), green($color), blue($color), .6);
    +  &:focus {
    +    border-color: $color;
    +    outline: 0;
    +    @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba);
    +  }
    +}
    +
    +// Form control sizing
    +//
    +// Relative text size, padding, and border-radii changes for form controls. For
    +// horizontal sizing, wrap controls in the predefined grid classes. ` receives focus
    +      // in IE and (under certain conditions) Edge.
    +      // See https://github.com/twbs/bootstrap/issues/19398.
    +      color: $input-color;
    +      background-color: $input-bg;
    +    }
    +  }
    +
    +  &[multiple],
    +  &[size]:not([size="1"]) {
    +    height: auto;
    +    padding-right: $custom-select-padding-x;
    +    background-image: none;
    +  }
    +
    +  &:disabled {
    +    color: $custom-select-disabled-color;
    +    background-color: $custom-select-disabled-bg;
    +  }
    +
    +  // Hides the default caret in IE11
    +  &::-ms-expand {
    +    display: none;
    +  }
    +
    +  // Remove outline from select box in FF
    +  &:-moz-focusring {
    +    color: transparent;
    +    text-shadow: 0 0 0 $custom-select-color;
    +  }
    +}
    +
    +.custom-select-sm {
    +  height: $custom-select-height-sm;
    +  padding-top: $custom-select-padding-y-sm;
    +  padding-bottom: $custom-select-padding-y-sm;
    +  padding-left: $custom-select-padding-x-sm;
    +  @include font-size($custom-select-font-size-sm);
    +}
    +
    +.custom-select-lg {
    +  height: $custom-select-height-lg;
    +  padding-top: $custom-select-padding-y-lg;
    +  padding-bottom: $custom-select-padding-y-lg;
    +  padding-left: $custom-select-padding-x-lg;
    +  @include font-size($custom-select-font-size-lg);
    +}
    +
    +
    +// File
    +//
    +// Custom file input.
    +
    +.custom-file {
    +  position: relative;
    +  display: inline-block;
    +  width: 100%;
    +  height: $custom-file-height;
    +  margin-bottom: 0;
    +}
    +
    +.custom-file-input {
    +  position: relative;
    +  z-index: 2;
    +  width: 100%;
    +  height: $custom-file-height;
    +  margin: 0;
    +  opacity: 0;
    +
    +  &:focus ~ .custom-file-label {
    +    border-color: $custom-file-focus-border-color;
    +    // box-shadow: $custom-file-focus-box-shadow;
    +    @include vp-box-shadow($custom-file-focus-box-shadow);
    +  }
    +
    +  // Use [disabled] and :disabled to work around https://github.com/twbs/bootstrap/issues/28247
    +  &[disabled] ~ .custom-file-label,
    +  &:disabled ~ .custom-file-label {
    +    background-color: $custom-file-disabled-bg;
    +  }
    +
    +  @each $lang, $value in $custom-file-text {
    +    &:lang(#{$lang}) ~ .custom-file-label::after {
    +      content: $value;
    +    }
    +  }
    +
    +  ~ .custom-file-label[data-browse]::after {
    +    content: attr(data-browse);
    +  }
    +}
    +
    +.custom-file-label {
    +  position: absolute;
    +  top: 0;
    +  right: 0;
    +  left: 0;
    +  z-index: 1;
    +  height: $custom-file-height;
    +  padding: $custom-file-padding-y $custom-file-padding-x;
    +  font-family: $custom-file-font-family;
    +  font-weight: $custom-file-font-weight;
    +  line-height: $custom-file-line-height;
    +  color: $custom-file-color;
    +  background-color: $custom-file-bg;
    +  border: $custom-file-border-width solid $custom-file-border-color;
    +  @include border-radius($custom-file-border-radius);
    +  @include box-shadow($custom-file-box-shadow);
    +
    +  &::after {
    +    position: absolute;
    +    top: 0;
    +    right: 0;
    +    bottom: 0;
    +    z-index: 3;
    +    display: block;
    +    height: $custom-file-height-inner;
    +    padding: $custom-file-padding-y $custom-file-padding-x;
    +    line-height: $custom-file-line-height;
    +    color: $custom-file-button-color;
    +    content: "Browse";
    +    @include gradient-bg($custom-file-button-bg);
    +    border-left: inherit;
    +    @include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0);
    +  }
    +}
    +
    +// Range
    +//
    +// Style range inputs the same across browsers. Vendor-specific rules for pseudo
    +// elements cannot be mixed. As such, there are no shared styles for focus or
    +// active states on prefixed selectors.
    +
    +.custom-range {
    +  width: 100%;
    +  height: add($custom-range-thumb-height, $custom-range-thumb-focus-box-shadow-width * 2);
    +  padding: 0; // Need to reset padding
    +  background-color: transparent;
    +  // appearance: none;
    +  @include vp-appearance(none);
    +  &:focus {
    +    outline: none;
    +
    +    // Pseudo-elements must be split across multiple rulesets to have an effect.
    +    // No box-shadow() mixin for focus accessibility.
    +    &::-webkit-slider-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
    +    &::-moz-range-thumb     { box-shadow: $custom-range-thumb-focus-box-shadow; }
    +    &::-ms-thumb            { box-shadow: $custom-range-thumb-focus-box-shadow; }
    +  }
    +
    +  &::-moz-focus-outer {
    +    border: 0;
    +  }
    +
    +  &::-webkit-slider-thumb {
    +    width: $custom-range-thumb-width;
    +    height: $custom-range-thumb-height;
    +    margin-top: ($custom-range-track-height - $custom-range-thumb-height) / 2; // Webkit specific
    +    @include gradient-bg($custom-range-thumb-bg);
    +    border: $custom-range-thumb-border;
    +    @include border-radius($custom-range-thumb-border-radius);
    +    @include box-shadow($custom-range-thumb-box-shadow);
    +    @include transition($custom-forms-transition);
    +    // appearance: none;
    +    @include vp-appearance(none);
    +
    +    &:active {
    +      @include gradient-bg($custom-range-thumb-active-bg);
    +    }
    +  }
    +
    +  &::-webkit-slider-runnable-track {
    +    width: $custom-range-track-width;
    +    height: $custom-range-track-height;
    +    color: transparent; // Why?
    +    cursor: $custom-range-track-cursor;
    +    background-color: $custom-range-track-bg;
    +    border-color: transparent;
    +    @include border-radius($custom-range-track-border-radius);
    +    @include box-shadow($custom-range-track-box-shadow);
    +  }
    +
    +  &::-moz-range-thumb {
    +    width: $custom-range-thumb-width;
    +    height: $custom-range-thumb-height;
    +    @include gradient-bg($custom-range-thumb-bg);
    +    border: $custom-range-thumb-border;
    +    @include border-radius($custom-range-thumb-border-radius);
    +    @include box-shadow($custom-range-thumb-box-shadow);
    +    @include transition($custom-forms-transition);
    +    // appearance: none;
    +    @include vp-appearance(none);
    +
    +    &:active {
    +      @include gradient-bg($custom-range-thumb-active-bg);
    +    }
    +  }
    +
    +  &::-moz-range-track {
    +    width: $custom-range-track-width;
    +    height: $custom-range-track-height;
    +    color: transparent;
    +    cursor: $custom-range-track-cursor;
    +    background-color: $custom-range-track-bg;
    +    border-color: transparent; // Firefox specific?
    +    @include border-radius($custom-range-track-border-radius);
    +    @include box-shadow($custom-range-track-box-shadow);
    +  }
    +
    +  &::-ms-thumb {
    +    width: $custom-range-thumb-width;
    +    height: $custom-range-thumb-height;
    +    margin-top: 0; // Edge specific
    +    margin-right: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.
    +    margin-left: $custom-range-thumb-focus-box-shadow-width;  // Workaround that overflowed box-shadow is hidden.
    +    @include gradient-bg($custom-range-thumb-bg);
    +    border: $custom-range-thumb-border;
    +    @include border-radius($custom-range-thumb-border-radius);
    +    @include box-shadow($custom-range-thumb-box-shadow);
    +    @include transition($custom-forms-transition);
    +    // appearance: none;
    +    @include vp-appearance(none);
    +
    +    &:active {
    +      @include gradient-bg($custom-range-thumb-active-bg);
    +    }
    +  }
    +
    +  &::-ms-track {
    +    width: $custom-range-track-width;
    +    height: $custom-range-track-height;
    +    color: transparent;
    +    cursor: $custom-range-track-cursor;
    +    background-color: transparent;
    +    border-color: transparent;
    +    border-width: $custom-range-thumb-height / 2;
    +    @include box-shadow($custom-range-track-box-shadow);
    +  }
    +
    +  &::-ms-fill-lower {
    +    background-color: $custom-range-track-bg;
    +    @include border-radius($custom-range-track-border-radius);
    +  }
    +
    +  &::-ms-fill-upper {
    +    margin-right: 15px; // arbitrary?
    +    background-color: $custom-range-track-bg;
    +    @include border-radius($custom-range-track-border-radius);
    +  }
    +
    +  &:disabled {
    +    &::-webkit-slider-thumb {
    +      background-color: $custom-range-thumb-disabled-bg;
    +    }
    +
    +    &::-webkit-slider-runnable-track {
    +      cursor: default;
    +    }
    +
    +    &::-moz-range-thumb {
    +      background-color: $custom-range-thumb-disabled-bg;
    +    }
    +
    +    &::-moz-range-track {
    +      cursor: default;
    +    }
    +
    +    &::-ms-thumb {
    +      background-color: $custom-range-thumb-disabled-bg;
    +    }
    +  }
    +}
    +
    +.custom-control-label::before,
    +.custom-file-label,
    +.custom-select {
    +  @include transition($custom-forms-transition);
    +}
    diff --git a/include/thirdparty/Bootstrap4/scss/bootstrap/_dropdown.scss b/include/thirdparty/Bootstrap4/scss/bootstrap/_dropdown.scss
    new file mode 100644
    index 0000000..9e8c8f8
    --- /dev/null
    +++ b/include/thirdparty/Bootstrap4/scss/bootstrap/_dropdown.scss
    @@ -0,0 +1,192 @@
    +// The dropdown wrapper (`
    `) +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; + + // Generate the caret automatically + @include caret(); +} + +// The dropdown menu +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: $zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: $dropdown-min-width; + padding: $dropdown-padding-y 0; + margin: $dropdown-spacer 0 0; // override default ul + @include font-size($dropdown-font-size); + color: $dropdown-color; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + list-style: none; + background-color: $dropdown-bg; + // background-clip: padding-box; + @include vp-background-clip(padding-box); + border: $dropdown-border-width solid $dropdown-border-color; + @include border-radius($dropdown-border-radius); + @include box-shadow($dropdown-box-shadow); +} + +@each $breakpoint in map-keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .dropdown-menu#{$infix}-left { + right: auto; + left: 0; + } + + .dropdown-menu#{$infix}-right { + right: 0; + left: auto; + } + } +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// Just add .dropup after the standard .dropdown class and you're set. +.dropup { + .dropdown-menu { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: $dropdown-spacer; + } + + .dropdown-toggle { + @include caret(up); + } +} + +.dropright { + .dropdown-menu { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: $dropdown-spacer; + } + + .dropdown-toggle { + @include caret(right); + &::after { + vertical-align: 0; + } + } +} + +.dropleft { + .dropdown-menu { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: $dropdown-spacer; + } + + .dropdown-toggle { + @include caret(left); + &::before { + vertical-align: 0; + } + } +} + +// When enabled Popper.js, reset basic dropdown position +// stylelint-disable-next-line no-duplicate-selectors +.dropdown-menu { + &[x-placement^="top"], + &[x-placement^="right"], + &[x-placement^="bottom"], + &[x-placement^="left"] { + right: auto; + bottom: auto; + } +} + +// Dividers (basically an `
    `) within the dropdown +.dropdown-divider { + @include nav-divider($dropdown-divider-bg, $dropdown-divider-margin-y, true); +} + +// Links, buttons, and more within the dropdown menu +// +// `
    ') + .on('mouseenter mouseleave', function(e) { + $(this).toggleClass('ui-state-hover', e.type == 'mouseenter'); + }) + .on('click', function() { + self.exec(cmd, data || hashes, {_userAction: true, _currentType: 'toast', _currentNode: $(this) }); + if (done) { + self.one(cmd+'done', function() { + if (typeof done === 'function') { + done(); + } else if (done === 'select') { + self.trigger('selectfiles', {files : inCwdHashes()}); + } + }); + } + }) + ); + } + delete t.action; + t.extNode = node; + return t; + }; + + if (! navigate.toast) { + navigate.toast = {}; + } + + !navigate.noselect && self.trigger('selectfiles', {files : self.searchStatus.state > 1 ? $.map(targets, function(f) { return f.hash; }) : hashes}); + + if (newItems.length) { + if (!navigate.noscroll) { + newItems.first().trigger('scrolltoview', {blink : false}); + self.resources.blink(newItems, 'lookme'); + } + if ($.isPlainObject(navigate.toast.incwd)) { + self.toast(makeToast(navigate.toast.incwd)); + } + } else { + if ($.isPlainObject(navigate.toast.inbuffer)) { + self.toast(makeToast(navigate.toast.inbuffer)); + } + } + }); + } + } + + dfrd.resolve(response); + + response.debug && self.debug('backend-debug', response); + }; + self.abortXHR(xhr); + lazy? self.lazy(resolve) : resolve(); + }, + xhr, _xhr, + xhrAbort = function(e) { + if (xhr && xhr.state() === 'pending') { + self.abortXHR(xhr, { quiet: true , abort: true }); + if (!e || (e.type !== 'unload' && e.type !== 'destroy')) { + self.autoSync(); + } + } + }, + abort = function(e){ + self.trigger(cmd + 'done'); + if (e.type == 'autosync') { + if (e.data.action != 'stop') return; + } else if (e.type != 'unload' && e.type != 'destroy' && e.type != 'openxhrabort') { + if (!e.data.added || !e.data.added.length) { + return; + } + } + xhrAbort(e); + }, + request = function(mode) { + var queueAbort = function() { + syncOnFail = false; + dfrd.reject(); + }; + + if (mode) { + if (mode === 'cmd') { + return cmd; + } + } + + if (isOpen) { + if (requestQueueSkipOpen) { + return dfrd.reject(); + } + requestQueueSkipOpen = true; + } + + dfrd.always(function() { + delete options.headers['X-elFinderReqid']; + }).fail(function(error, xhr, response) { + var errData = { + cmd: cmd, + err: error, + xhr: xhr, + rc: response + }, errMsg; + + // unset this cmd queue when user canceling + // see notify : function - `cancel.reject(0);` + if (error === 0) { + if (requestQueue.length) { + requestQueue = $.grep(requestQueue, function(req) { + return (req('cmd') === cmd) ? false : true; + }); + } + } + // trigger "requestError" event + self.trigger('requestError', errData); + if (errData._getEvent && errData._getEvent().isDefaultPrevented()) { + deffail = false; + syncOnFail = false; + if (error) { + error.error = ''; + } + } + // abort xhr + xhrAbort(); + if (isOpen) { + openDir = self.file(data.target); + openDir && openDir.volumeid && self.isRoot(openDir) && delete self.volumeExpires[openDir.volumeid]; + } + self.trigger(cmd + 'fail', response); + errMsg = (typeof error === 'object')? error.error : error; + if (errMsg) { + deffail ? self.error(errMsg) : self.debug('error', self.i18n(errMsg)); + } + syncOnFail && self.sync(); + }); + + if (!cmd) { + syncOnFail = false; + return dfrd.reject({error :'errCmdReq'}); + } + + if (self.maxTargets && data.targets && data.targets.length > self.maxTargets) { + syncOnFail = false; + return dfrd.reject({error :['errMaxTargets', self.maxTargets]}); + } + + defdone && dfrd.done(done); + + // quiet abort not completed "open" requests + if (isOpen) { + while ((_xhr = queue.pop())) { + _xhr.queueAbort(); + } + if (cwd !== data.target) { + while ((_xhr = cwdQueue.pop())) { + _xhr.queueAbort(); + } + } + } + + // trigger abort autoSync for commands to add the item + if ($.inArray(cmd, (self.cmdsToAdd + ' autosync').split(' ')) !== -1) { + if (cmd !== 'autosync') { + self.autoSync('stop'); + dfrd.always(function() { + self.autoSync(); + }); + } + self.trigger('openxhrabort'); + } + + delete options.preventFail; + + if (self.api >= 2.1029) { + if (useCache) { + options.headers['X-elFinderReqid'] = reqId; + } else { + Object.assign(options.data, { reqid : reqId }); + } + } + + // function for set value of this syncOnFail + dfrd.syncOnFail = function(state) { + syncOnFail = !!state; + }; + + requestCnt++; + + dfrd.xhr = xhr = self.transport.send(options).always(function() { + // set responseURL from native xhr object + if (options._xhr && typeof options._xhr.responseURL !== 'undefined') { + xhr.responseURL = options._xhr.responseURL || ''; + } + --requestCnt; + if (requestQueue.length) { + requestQueue.shift()(); + } else { + requestQueueSkipOpen = false; + } + }).fail(error).done(success); + + if (self.api >= 2.1029) { + xhr._requestId = reqId; + } + + if (isOpen || (data.compare && cmd === 'info')) { + // regist function queueAbort + xhr.queueAbort = queueAbort; + // add autoSync xhr into queue + queue.unshift(xhr); + // bind abort() + data.compare && self.bind(self.cmdsToAdd + ' autosync openxhrabort', abort); + dfrd.always(function() { + var ndx = $.inArray(xhr, queue); + data.compare && self.unbind(self.cmdsToAdd + ' autosync openxhrabort', abort); + ndx !== -1 && queue.splice(ndx, 1); + }); + } else if ($.inArray(cmd, self.abortCmdsOnOpen) !== -1) { + // regist function queueAbort + xhr.queueAbort = queueAbort; + // add "open" xhr, only cwd xhr into queue + cwdQueue.unshift(xhr); + dfrd.always(function() { + var ndx = $.inArray(xhr, cwdQueue); + ndx !== -1 && cwdQueue.splice(ndx, 1); + }); + } + + // abort pending xhr on window unload or elFinder destroy + self.bind('unload destroy', abort); + dfrd.always(function() { + self.unbind('unload destroy', abort); + }); + + return dfrd; + }, + queueingRequest = function() { + // show notify + if (notify.type && notify.cnt) { + if (cancel) { + notify.cancel = dfrd; + opts.eachCancel && (notify.id = +new Date()); + } + timeout = setTimeout(function() { + self.notify(notify); + dfrd.always(function() { + notify.cnt = -(parseInt(notify.cnt)||0); + self.notify(notify); + }); + }, self.notifyDelay); + + dfrd.always(function() { + clearTimeout(timeout); + }); + } + // queueing + if (isOpen) { + requestQueueSkipOpen = false; + } + if (requestCnt < requestMaxConn) { + // do request + return request(); + } else { + if (isOpen) { + requestQueue.unshift(request); + } else { + requestQueue.push(request); + } + return dfrd; + } + }, + bindData = {opts: opts, result: true}, + openDir; + + // prevent request initial request is completed + if (!self.api && !data.init) { + syncOnFail = false; + return dfrd.reject(); + } + + // trigger "request.cmd" that callback be able to cancel request by substituting "false" for "event.data.result" + self.trigger('request.' + cmd, bindData, true); + + if (! bindData.result) { + self.trigger(cmd + 'done'); + return dfrd.reject(); + } else if (typeof bindData.result === 'object' && bindData.result.promise) { + bindData.result + .done(queueingRequest) + .fail(function() { + self.trigger(cmd + 'done'); + dfrd.reject(); + }); + return dfrd; + } + + return queueingRequest(); + }; + + /** + * Call cache() + * Store info about files/dirs in "files" object. + * + * @param Array files + * @return void + */ + this.cache = function(dataArray) { + if (! Array.isArray(dataArray)) { + dataArray = [ dataArray ]; + } + cache(dataArray); + }; + + /** + * Update file object caches by respose data object + * + * @param Object respose data object + * @return void + */ + this.updateCache = function(data) { + if ($.isPlainObject(data)) { + data.files && data.files.length && cache(data.files, 'files'); + data.tree && data.tree.length && cache(data.tree, 'tree'); + data.removed && data.removed.length && remove(data.removed); + data.added && data.added.length && cache(data.added, 'add'); + data.changed && data.changed.length && change(data.changed, 'change'); + } + }; + + /** + * Compare current files cache with new files and return diff + * + * @param Array new files + * @param String target folder hash + * @param Array exclude properties to compare + * @return Object + */ + this.diff = function(incoming, onlydir, excludeProps) { + var raw = {}, + added = [], + removed = [], + changed = [], + excludes = null, + isChanged = function(hash) { + var l = changed.length; + + while (l--) { + if (changed[l].hash == hash) { + return true; + } + } + }; + + $.each(incoming, function(i, f) { + raw[f.hash] = f; + }); + + // make excludes object + if (excludeProps && excludeProps.length) { + excludes = {}; + $.each(excludeProps, function() { + excludes[this] = true; + }); + } + + // find removed + $.each(files, function(hash, f) { + if (! raw[hash] && (! onlydir || f.phash === onlydir)) { + removed.push(hash); + } + }); + + // compare files + $.each(raw, function(hash, file) { + var origin = files[hash], + orgKeys = {}, + chkKeyLen; + + if (!origin) { + added.push(file); + } else { + // make orgKeys object + $.each(Object.keys(origin), function() { + orgKeys[this] = true; + }); + $.each(file, function(prop) { + delete orgKeys[prop]; + if (! excludes || ! excludes[prop]) { + if (file[prop] !== origin[prop]) { + changed.push(file); + orgKeys = {}; + return false; + } + } + }); + chkKeyLen = Object.keys(orgKeys).length; + if (chkKeyLen !== 0) { + if (excludes) { + $.each(orgKeys, function(prop) { + if (excludes[prop]) { + --chkKeyLen; + } + }); + } + (chkKeyLen !== 0) && changed.push(file); + } + } + }); + + // parents of removed dirs mark as changed (required for tree correct work) + $.each(removed, function(i, hash) { + var file = files[hash], + phash = file.phash; + + if (phash + && file.mime == 'directory' + && $.inArray(phash, removed) === -1 + && raw[phash] + && !isChanged(phash)) { + changed.push(raw[phash]); + } + }); + + return { + added : added, + removed : removed, + changed : changed + }; + }; + + /** + * Sync content + * + * @return jQuery.Deferred + */ + this.sync = function(onlydir, polling) { + this.autoSync('stop'); + var self = this, + compare = function(){ + var c = '', cnt = 0, mtime = 0; + if (onlydir && polling) { + $.each(files, function(h, f) { + if (f.phash && f.phash === onlydir) { + ++cnt; + mtime = Math.max(mtime, f.ts); + } + c = cnt+':'+mtime; + }); + } + return c; + }, + comp = compare(), + dfrd = $.Deferred().done(function() { self.trigger('sync'); }), + opts = [this.request({ + data : {cmd : 'open', reload : 1, target : cwd, tree : (! onlydir && this.ui.tree) ? 1 : 0, compare : comp}, + preventDefault : true + })], + exParents = function() { + var parents = [], + curRoot = self.file(self.root(cwd)), + curId = curRoot? curRoot.volumeid : null, + phash = self.cwd().phash, + isroot,pdir; + + while(phash) { + if (pdir = self.file(phash)) { + if (phash.indexOf(curId) !== 0) { + parents.push( {target: phash, cmd: 'tree'} ); + if (! self.isRoot(pdir)) { + parents.push( {target: phash, cmd: 'parents'} ); + } + curRoot = self.file(self.root(phash)); + curId = curRoot? curRoot.volumeid : null; + } + phash = pdir.phash; + } else { + phash = null; + } + } + return parents; + }; + + if (! onlydir && self.api >= 2) { + (cwd !== this.root()) && opts.push(this.request({ + data : {cmd : 'parents', target : cwd}, + preventDefault : true + })); + $.each(exParents(), function(i, data) { + opts.push(self.request({ + data : {cmd : data.cmd, target : data.target}, + preventDefault : true + })); + }); + } + $.when.apply($, opts) + .fail(function(error, xhr) { + if (! polling || $.inArray('errOpen', error) !== -1) { + dfrd.reject(error); + self.parseError(error) && self.request({ + data : {cmd : 'open', target : (self.lastDir('') || self.root()), tree : 1, init : 1}, + notify : {type : 'open', cnt : 1, hideCnt : true} + }); + } else { + dfrd.reject((error && xhr.status != 0)? error : void 0); + } + }) + .done(function(odata) { + var pdata, argLen, i; + + if (odata.cwd.compare) { + if (comp === odata.cwd.compare) { + return dfrd.reject(); + } + } + + // for 2nd and more requests + pdata = {tree : []}; + + // results marge of 2nd and more requests + argLen = arguments.length; + if (argLen > 1) { + for(i = 1; i < argLen; i++) { + if (arguments[i].tree && arguments[i].tree.length) { + pdata.tree.push.apply(pdata.tree, arguments[i].tree); + } + } + } + + if (self.api < 2.1) { + if (! pdata.tree) { + pdata.tree = []; + } + pdata.tree.push(odata.cwd); + } + + // data normalize + odata = self.normalize(odata); + if (!self.validResponse('open', odata)) { + return dfrd.reject((odata.norError || 'errResponse')); + } + pdata = self.normalize(pdata); + if (!self.validResponse('tree', pdata)) { + return dfrd.reject((pdata.norError || 'errResponse')); + } + + var diff = self.diff(odata.files.concat(pdata && pdata.tree ? pdata.tree : []), onlydir); + + diff.added.push(odata.cwd); + + self.updateCache(diff); + + // trigger events + diff.removed.length && self.remove(diff); + diff.added.length && self.add(diff); + diff.changed.length && self.change(diff); + return dfrd.resolve(diff); + }) + .always(function() { + self.autoSync(); + }); + + return dfrd; + }; + + this.upload = function(files) { + return this.transport.upload(files, this); + }; + + /** + * Bind keybord shortcut to keydown event + * + * @example + * elfinder.shortcut({ + * pattern : 'ctrl+a', + * description : 'Select all files', + * callback : function(e) { ... }, + * keypress : true|false (bind to keypress instead of keydown) + * }) + * + * @param Object shortcut config + * @return elFinder + */ + this.shortcut = function(s) { + var patterns, pattern, code, i, parts; + + if (this.options.allowShortcuts && s.pattern && $.isFunction(s.callback)) { + patterns = s.pattern.toUpperCase().split(/\s+/); + + for (i= 0; i < patterns.length; i++) { + pattern = patterns[i]; + parts = pattern.split('+'); + code = (code = parts.pop()).length == 1 + ? (code > 0 ? code : code.charCodeAt(0)) + : (code > 0 ? code : $.ui.keyCode[code]); + + if (code && !shortcuts[pattern]) { + shortcuts[pattern] = { + keyCode : code, + altKey : $.inArray('ALT', parts) != -1, + ctrlKey : $.inArray('CTRL', parts) != -1, + shiftKey : $.inArray('SHIFT', parts) != -1, + type : s.type || 'keydown', + callback : s.callback, + description : s.description, + pattern : pattern + }; + } + } + } + return this; + }; + + /** + * Registered shortcuts + * + * @type Object + **/ + this.shortcuts = function() { + var ret = []; + + $.each(shortcuts, function(i, s) { + ret.push([s.pattern, self.i18n(s.description)]); + }); + return ret; + }; + + /** + * Get/set clipboard content. + * Return new clipboard content. + * + * @example + * this.clipboard([]) - clean clipboard + * this.clipboard([{...}, {...}], true) - put 2 files in clipboard and mark it as cutted + * + * @param Array new files hashes + * @param Boolean cut files? + * @return Array + */ + this.clipboard = function(hashes, cut) { + var map = function() { return $.map(clipboard, function(f) { return f.hash; }); }; + + if (hashes !== void(0)) { + clipboard.length && this.trigger('unlockfiles', {files : map()}); + remember = {}; + + clipboard = $.map(hashes||[], function(hash) { + var file = files[hash]; + if (file) { + + remember[hash] = true; + + return { + hash : hash, + phash : file.phash, + name : file.name, + mime : file.mime, + read : file.read, + locked : file.locked, + cut : !!cut + }; + } + return null; + }); + this.trigger('changeclipboard', {clipboard : clipboard.slice(0, clipboard.length)}); + cut && this.trigger('lockfiles', {files : map()}); + } + + // return copy of clipboard instead of refrence + return clipboard.slice(0, clipboard.length); + }; + + /** + * Return true if command enabled + * + * @param String command name + * @param String|void hash for check of own volume's disabled cmds + * @return Boolean + */ + this.isCommandEnabled = function(name, dstHash) { + var disabled, cmd, + cvid = self.cwd().volumeid || ''; + + // In serach results use selected item hash to check + if (!dstHash && self.searchStatus.state > 1 && self.selected().length) { + dstHash = self.selected()[0]; + } + if (dstHash && (! cvid || dstHash.indexOf(cvid) !== 0)) { + disabled = self.option('disabledFlip', dstHash); + //if (! disabled) { + // disabled = {}; + //} + } else { + disabled = cwdOptions.disabledFlip/* || {}*/; + } + cmd = this._commands[name]; + return cmd ? (cmd.alwaysEnabled || !disabled[name]) : false; + }; + + /** + * Exec command and return result; + * + * @param String command name + * @param String|Array usualy files hashes + * @param String|Array command options + * @param String|void hash for enabled check of own volume's disabled cmds + * @return $.Deferred + */ + this.exec = function(cmd, files, opts, dstHash) { + var dfrd, resType; + + // apply commandMap for keyboard shortcut + if (!dstHash && this.commandMap[cmd] && this.commandMap[cmd] !== 'hidden') { + cmd = this.commandMap[cmd]; + } + + if (cmd === 'open') { + if (this.searchStatus.state || this.searchStatus.ininc) { + this.trigger('searchend', { noupdate: true }); + } + this.autoSync('stop'); + } + if (!dstHash && files) { + if ($.isArray(files)) { + if (files.length) { + dstHash = files[0]; + } + } else { + dstHash = files; + } + } + dfrd = this._commands[cmd] && this.isCommandEnabled(cmd, dstHash) + ? this._commands[cmd].exec(files, opts) + : $.Deferred().reject('errUnknownCmd'); + + resType = typeof dfrd; + if (!(resType === 'object' && dfrd.promise)) { + self.debug('warning', '"cmd.exec()" should be returned "$.Deferred" but cmd "' + cmd + '" returned "' + resType + '"'); + dfrd = $.Deferred().resolve(); + } + + this.trigger('exec', { dfrd : dfrd, cmd : cmd, files : files, opts : opts, dstHash : dstHash }); + return dfrd; + }; + + /** + * Create and return dialog. + * + * @param String|DOMElement dialog content + * @param Object dialog options + * @return jQuery + */ + this.dialog = function(content, options) { + var dialog = $('
    ').append(content).appendTo(node).elfinderdialog(options, self), + dnode = dialog.closest('.ui-dialog'), + resize = function(){ + ! dialog.data('draged') && dialog.is(':visible') && dialog.elfinderdialog('posInit'); + }; + if (dnode.length) { + self.bind('resize', resize); + dnode.on('remove', function() { + self.unbind('resize', resize); + }); + } + return dialog; + }; + + /** + * Create and return toast. + * + * @param Object toast options - see ui/toast.js + * @return jQuery + */ + this.toast = function(options) { + return $('
    ').appendTo(this.ui.toast).elfindertoast(options || {}, this); + }; + + /** + * Return UI widget or node + * + * @param String ui name + * @return jQuery + */ + this.getUI = function(ui) { + return ui? (this.ui[ui] || $()) : node; + }; + + /** + * Return elFinder.command instance or instances array + * + * @param String command name + * @return Object | Array + */ + this.getCommand = function(name) { + return name === void(0) ? this._commands : this._commands[name]; + }; + + /** + * Resize elfinder node + * + * @param String|Number width + * @param String|Number height + * @return void + */ + this.resize = function(w, h) { + var getMargin = function() { + var m = node.outerHeight(true) - node.innerHeight(), + p = node; + + while(p.get(0) !== heightBase.get(0)) { + p = p.parent(); + m += p.outerHeight(true) - p.innerHeight(); + if (! p.parent().length) { + // reached the document + break; + } + } + return m; + }, + fit = ! node.hasClass('ui-resizable'), + prv = node.data('resizeSize') || {w: 0, h: 0}, + mt, size = {}; + + if (heightBase && heightBase.data('resizeTm')) { + clearTimeout(heightBase.data('resizeTm')); + } + + if (typeof h === 'string') { + if (mt = h.match(/^([0-9.]+)%$/)) { + // setup heightBase + if (! heightBase || ! heightBase.length) { + heightBase = $(window); + } + if (! heightBase.data('marginToMyNode')) { + heightBase.data('marginToMyNode', getMargin()); + } + if (! heightBase.data('fitToBaseFunc')) { + heightBase.data('fitToBaseFunc', function(e) { + var tm = heightBase.data('resizeTm'); + e.preventDefault(); + e.stopPropagation(); + tm && cancelAnimationFrame(tm); + if (! node.hasClass('elfinder-fullscreen') && (!self.UA.Mobile || heightBase.data('rotated') !== self.UA.Rotated)) { + heightBase.data('rotated', self.UA.Rotated); + heightBase.data('resizeTm', requestAnimationFrame(function() { + self.restoreSize(); + })); + } + }); + } + if (typeof heightBase.data('rotated') === 'undefined') { + heightBase.data('rotated', self.UA.Rotated); + } + h = heightBase.height() * (mt[1] / 100) - heightBase.data('marginToMyNode'); + + heightBase.off('resize.' + self.namespace, heightBase.data('fitToBaseFunc')); + fit && heightBase.on('resize.' + self.namespace, heightBase.data('fitToBaseFunc')); + } + } + + node.css({ width : w, height : parseInt(h) }); + size.w = Math.round(node.width()); + size.h = Math.round(node.height()); + node.data('resizeSize', size); + if (size.w !== prv.w || size.h !== prv.h) { + node.trigger('resize'); + this.trigger('resize', {width : size.w, height : size.h}); + } + }; + + /** + * Restore elfinder node size + * + * @return elFinder + */ + this.restoreSize = function() { + this.resize(width, height); + }; + + this.show = function() { + node.show(); + this.enable().trigger('show'); + }; + + this.hide = function() { + if (this.options.enableAlways) { + prevEnabled = enabled; + enabled = false; + } + this.disable(); + this.trigger('hide'); + node.hide(); + }; + + /** + * Lazy execution function + * + * @param Object function + * @param Number delay + * @param Object options + * @return Object jQuery.Deferred + */ + this.lazy = function(func, delay, opts) { + var busy = function(state) { + var cnt = node.data('lazycnt'), + repaint; + + if (state) { + repaint = node.data('lazyrepaint')? false : opts.repaint; + if (! cnt) { + node.data('lazycnt', 1) + .addClass('elfinder-processing'); + } else { + node.data('lazycnt', ++cnt); + } + if (repaint) { + node.data('lazyrepaint', true).css('display'); // force repaint + } + } else { + if (cnt && cnt > 1) { + node.data('lazycnt', --cnt); + } else { + repaint = node.data('lazyrepaint'); + node.data('lazycnt', 0) + .removeData('lazyrepaint') + .removeClass('elfinder-processing'); + repaint && node.css('display'); // force repaint; + self.trigger('lazydone'); + } + } + }, + dfd = $.Deferred(), + callFunc = function() { + dfd.resolve(func.call(dfd)); + busy(false); + }; + + delay = delay || 0; + opts = opts || {}; + busy(true); + + if (delay) { + setTimeout(callFunc, delay); + } else { + requestAnimationFrame(callFunc); + } + + return dfd; + }; + + /** + * Destroy this elFinder instance + * + * @return void + **/ + this.destroy = function() { + if (node && node[0].elfinder) { + node.hasClass('elfinder-fullscreen') && self.toggleFullscreen(node); + this.options.syncStart = false; + this.autoSync('forcestop'); + this.trigger('destroy').disable(); + clipboard = []; + selected = []; + listeners = {}; + shortcuts = {}; + $(window).off('.' + namespace); + $(document).off('.' + namespace); + self.trigger = function(){}; + $(beeper).remove(); + node.off() + .removeData() + .empty() + .append(prevContent.contents()) + .attr('class', prevContent.attr('class')) + .attr('style', prevContent.attr('style')); + delete node[0].elfinder; + // restore kept events + $.each(prevEvents, function(n, arr) { + $.each(arr, function(i, o) { + node.on(o.type + (o.namespace? '.'+o.namespace : ''), o.selector, o.handler); + }); + }); + } + }; + + /** + * Start or stop auto sync + * + * @param String|Bool stop + * @return void + */ + this.autoSync = function(mode) { + var sync; + if (self.options.sync >= 1000) { + if (syncInterval) { + clearTimeout(syncInterval); + syncInterval = null; + self.trigger('autosync', {action : 'stop'}); + } + + if (mode === 'stop') { + ++autoSyncStop; + } else { + autoSyncStop = Math.max(0, --autoSyncStop); + } + + if (autoSyncStop || mode === 'forcestop' || ! self.options.syncStart) { + return; + } + + // run interval sync + sync = function(start){ + var timeout; + if (cwdOptions.syncMinMs && (start || syncInterval)) { + start && self.trigger('autosync', {action : 'start'}); + timeout = Math.max(self.options.sync, cwdOptions.syncMinMs); + syncInterval && clearTimeout(syncInterval); + syncInterval = setTimeout(function() { + var dosync = true, hash = cwd, cts; + if (cwdOptions.syncChkAsTs && files[hash] && (cts = files[hash].ts)) { + self.request({ + data : {cmd : 'info', targets : [hash], compare : cts, reload : 1}, + preventDefault : true + }) + .done(function(data){ + var ts; + dosync = true; + if (data.compare) { + ts = data.compare; + if (ts == cts) { + dosync = false; + } + } + if (dosync) { + self.sync(hash).always(function(){ + if (ts) { + // update ts for cache clear etc. + files[hash].ts = ts; + } + sync(); + }); + } else { + sync(); + } + }) + .fail(function(error, xhr){ + var err = self.parseError(error); + if (err && xhr.status != 0) { + self.error(err); + if (Array.isArray(err) && $.inArray('errOpen', err) !== -1) { + self.request({ + data : {cmd : 'open', target : (self.lastDir('') || self.root()), tree : 1, init : 1}, + notify : {type : 'open', cnt : 1, hideCnt : true} + }); + } + } else { + syncInterval = setTimeout(function() { + sync(); + }, timeout); + } + }); + } else { + self.sync(cwd, true).always(function(){ + sync(); + }); + } + }, timeout); + } + }; + sync(true); + } + }; + + /** + * Return bool is inside work zone of specific point + * + * @param Number event.pageX + * @param Number event.pageY + * @return Bool + */ + this.insideWorkzone = function(x, y, margin) { + var rectangle = this.getUI('workzone').data('rectangle'); + + margin = margin || 1; + if (x < rectangle.left + margin + || x > rectangle.left + rectangle.width + margin + || y < rectangle.top + margin + || y > rectangle.top + rectangle.height + margin) { + return false; + } + return true; + }; + + /** + * Target ui node move to last of children of elFinder node fot to show front + * + * @param Object target Target jQuery node object + */ + this.toFront = function(target) { + var nodes = node.children('.ui-front').removeClass('elfinder-frontmost'), + lastnode = nodes.last(); + nodes.css('z-index', ''); + $(target).addClass('ui-front elfinder-frontmost').css('z-index', lastnode.css('z-index') + 1); + }; + + /** + * Remove class 'elfinder-frontmost' and hide() to target ui node + * + * @param Object target Target jQuery node object + * @param Boolean nohide Do not hide + */ + this.toHide =function(target, nohide) { + var tgt = $(target), + last; + + !nohide && tgt.hide(); + if (tgt.hasClass('elfinder-frontmost')) { + tgt.removeClass('elfinder-frontmost'); + last = node.children('.ui-front:visible:not(.elfinder-frontmost)').last(); + if (last.length) { + requestAnimationFrame(function() { + if (!node.children('.elfinder-frontmost:visible').length) { + self.toFront(last); + last.trigger('frontmost'); + } + }); + } + } + }; + + /** + * Return css object for maximize + * + * @return Object + */ + this.getMaximizeCss = function() { + return { + width : '100%', + height : '100%', + margin : 0, + top : 0, + left : 0, + display : 'block', + position: 'fixed', + zIndex : Math.max(self.zIndex? (self.zIndex + 1) : 0 , 1000), + maxWidth : '', + maxHeight: '' + }; + }; + + // Closure for togglefullscreen + (function() { + // check is in iframe + if (inFrame && self.UA.Fullscreen) { + self.UA.Fullscreen = false; + if (parentIframe && typeof parentIframe.attr('allowfullscreen') !== 'undefined') { + self.UA.Fullscreen = true; + } + } + + var orgStyle, bodyOvf, resizeTm, fullElm, exitFull, toFull, funcObj, + cls = 'elfinder-fullscreen', + clsN = 'elfinder-fullscreen-native', + checkDialog = function() { + var t = 0, + l = 0; + $.each(node.children('.ui-dialog,.ui-draggable'), function(i, d) { + var $d = $(d), + pos = $d.position(); + + if (pos.top < 0) { + $d.css('top', t); + t += 20; + } + if (pos.left < 0) { + $d.css('left', l); + l += 20; + } + }); + }, + setFuncObj = function() { + var useFullscreen = self.storage('useFullscreen'); + funcObj = self.UA.Fullscreen && (useFullscreen? useFullscreen > 0 : self.options.commandsOptions.fullscreen.mode === 'screen') ? { + // native full screen mode + + fullElm: function() { + return document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement || null; + }, + + exitFull: function() { + if (document.exitFullscreen) { + return document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + return document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + return document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + return document.msExitFullscreen(); + } + }, + + toFull: function(elem) { + if (elem.requestFullscreen) { + return elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + return elem.webkitRequestFullscreen(); + } else if (elem.mozRequestFullScreen) { + return elem.mozRequestFullScreen(); + } else if (elem.msRequestFullscreen) { + return elem.msRequestFullscreen(); + } + return false; + } + } : { + // node element maximize mode + + fullElm: function() { + var full; + if (node.hasClass(cls)) { + return node.get(0); + } else { + full = node.find('.' + cls); + if (full.length) { + return full.get(0); + } + } + return null; + }, + + exitFull: function() { + var elm; + + $(window).off('resize.' + namespace, resize); + if (bodyOvf !== void(0)) { + $('body').css('overflow', bodyOvf); + } + bodyOvf = void(0); + + if (orgStyle) { + elm = orgStyle.elm; + restoreStyle(elm); + $(elm).trigger('resize', {fullscreen: 'off'}); + } + + $(window).trigger('resize'); + }, + + toFull: function(elem) { + bodyOvf = $('body').css('overflow') || ''; + $('body').css('overflow', 'hidden'); + + $(elem).css(self.getMaximizeCss()) + .addClass(cls) + .trigger('resize', {fullscreen: 'on'}); + + checkDialog(); + + $(window).on('resize.' + namespace, resize).trigger('resize'); + + return true; + } + }; + }, + restoreStyle = function(elem) { + if (orgStyle && orgStyle.elm == elem) { + $(elem).removeClass(cls + ' ' + clsN).attr('style', orgStyle.style); + orgStyle = null; + } + }, + resize = function(e) { + var elm; + if (e.target === window) { + resizeTm && cancelAnimationFrame(resizeTm); + resizeTm = requestAnimationFrame(function() { + if (elm = funcObj.fullElm()) { + $(elm).trigger('resize', {fullscreen: 'on'}); + } + }); + } + }; + + setFuncObj(); + + $(document).on('fullscreenchange.' + namespace + ' webkitfullscreenchange.' + namespace + ' mozfullscreenchange.' + namespace + ' MSFullscreenChange.' + namespace, function(e){ + if (self.UA.Fullscreen) { + var elm = funcObj.fullElm(), + win = $(window); + + resizeTm && cancelAnimationFrame(resizeTm); + if (elm === null) { + win.off('resize.' + namespace, resize); + if (orgStyle) { + elm = orgStyle.elm; + restoreStyle(elm); + $(elm).trigger('resize', {fullscreen: 'off'}); + } + } else { + $(elm).addClass(cls + ' ' + clsN) + .attr('style', 'width:100%; height:100%; margin:0; padding:0;') + .trigger('resize', {fullscreen: 'on'}); + win.on('resize.' + namespace, resize); + checkDialog(); + } + win.trigger('resize'); + } + }); + + /** + * Toggle Full Scrren Mode + * + * @param Object target + * @param Bool full + * @return Object | Null DOM node object of current full scrren + */ + self.toggleFullscreen = function(target, full) { + var elm = $(target).get(0), + curElm = null; + + curElm = funcObj.fullElm(); + if (curElm) { + if (curElm == elm) { + if (full === true) { + return curElm; + } + } else { + if (full === false) { + return curElm; + } + } + funcObj.exitFull(); + return null; + } else { + if (full === false) { + return null; + } + } + + setFuncObj(); + orgStyle = {elm: elm, style: $(elm).attr('style')}; + if (funcObj.toFull(elm) !== false) { + return elm; + } else { + orgStyle = null; + return null; + } + }; + })(); + + // Closure for toggleMaximize + (function(){ + var cls = 'elfinder-maximized', + resizeTm, + resize = function(e) { + if (e.target === window && e.data && e.data.elm) { + var elm = e.data.elm; + resizeTm && cancelAnimationFrame(resizeTm); + resizeTm = requestAnimationFrame(function() { + elm.trigger('resize', {maximize: 'on'}); + }); + } + }, + exitMax = function(elm) { + $(window).off('resize.' + namespace, resize); + $('body').css('overflow', elm.data('bodyOvf')); + elm.removeClass(cls) + .attr('style', elm.data('orgStyle')) + .removeData('bodyOvf') + .removeData('orgStyle'); + elm.trigger('resize', {maximize: 'off'}); + }, + toMax = function(elm) { + elm.data('bodyOvf', $('body').css('overflow') || '') + .data('orgStyle', elm.attr('style')) + .addClass(cls) + .css(self.getMaximizeCss()); + $('body').css('overflow', 'hidden'); + $(window).on('resize.' + namespace, {elm: elm}, resize); + elm.trigger('resize', {maximize: 'on'}); + }; + + /** + * Toggle Maximize target node + * + * @param Object target + * @param Bool max + * @return void + */ + self.toggleMaximize = function(target, max) { + var elm = $(target), + maximized = elm.hasClass(cls); + + if (maximized) { + if (max === true) { + return; + } + exitMax(elm); + } else { + if (max === false) { + return; + } + toMax(elm); + } + }; + })(); + + /************* init stuffs ****************/ + Object.assign($.ui.keyCode, { + 'F1' : 112, + 'F2' : 113, + 'F3' : 114, + 'F4' : 115, + 'F5' : 116, + 'F6' : 117, + 'F7' : 118, + 'F8' : 119, + 'F9' : 120, + 'F10' : 121, + 'F11' : 122, + 'F12' : 123, + 'DIG0' : 48, + 'DIG1' : 49, + 'DIG2' : 50, + 'DIG3' : 51, + 'DIG4' : 52, + 'DIG5' : 53, + 'DIG6' : 54, + 'DIG7' : 55, + 'DIG8' : 56, + 'DIG9' : 57, + 'NUM0' : 96, + 'NUM1' : 97, + 'NUM2' : 98, + 'NUM3' : 99, + 'NUM4' : 100, + 'NUM5' : 101, + 'NUM6' : 102, + 'NUM7' : 103, + 'NUM8' : 104, + 'NUM9' : 105, + 'CONTEXTMENU' : 93, + 'DOT' : 190 + }); + + this.dragUpload = false; + this.xhrUpload = (typeof XMLHttpRequestUpload != 'undefined' || typeof XMLHttpRequestEventTarget != 'undefined') && typeof File != 'undefined' && typeof FormData != 'undefined'; + + // configure transport object + this.transport = {}; + + if (typeof(this.options.transport) == 'object') { + this.transport = this.options.transport; + if (typeof(this.transport.init) == 'function') { + this.transport.init(this); + } + } + + if (typeof(this.transport.send) != 'function') { + this.transport.send = function(opts) { + if (!self.UA.IE) { + // keep native xhr object for handling property responseURL + opts._xhr = new XMLHttpRequest(); + opts.xhr = function() { return opts._xhr; }; + } + return $.ajax(opts); + }; + } + + if (this.transport.upload == 'iframe') { + this.transport.upload = $.proxy(this.uploads.iframe, this); + } else if (typeof(this.transport.upload) == 'function') { + this.dragUpload = !!this.options.dragUploadAllow; + } else if (this.xhrUpload && !!this.options.dragUploadAllow) { + this.transport.upload = $.proxy(this.uploads.xhr, this); + this.dragUpload = true; + } else { + this.transport.upload = $.proxy(this.uploads.iframe, this); + } + + /** + * Decoding 'raw' string converted to unicode + * + * @param String str + * @return String + */ + this.decodeRawString = function(str) { + var charCodes = function(str) { + var i, len, arr; + for (i=0,len=str.length,arr=[]; i= 0xd800 && c <= 0xdbff) { + scalars.push((c & 1023) + 64 << 10 | arr[++i] & 1023); + } else { + scalars.push(c); + } + } + return scalars; + }, + decodeUTF8 = function(arr) { + var i, len, c, str, char = String.fromCharCode; + for (i=0,len=arr.length,str=""; c=arr[i],i= 0xc2) { + str += char((c&31)<<6 | arr[++i]&63); + } else if (c <= 0xef && c >= 0xe0) { + str += char((c&15)<<12 | (arr[++i]&63)<<6 | arr[++i]&63); + } else if (c <= 0xf7 && c >= 0xf0) { + str += char( + 0xd800 | ((c&7)<<8 | (arr[++i]&63)<<2 | arr[++i]>>>4&3) - 64, + 0xdc00 | (arr[i++]&15)<<6 | arr[i]&63 + ); + } else { + str += char(0xfffd); + } + } + return str; + }; + + return decodeUTF8(scalarValues(str)); + }; + + /** + * Gets target file contents by file.hash + * + * @param String hash The hash + * @param String responseType 'blob' or 'arraybuffer' (default) + * @return arraybuffer|blob The contents. + */ + this.getContents = function(hash, responseType) { + var self = this, + dfd = $.Deferred(), + type = responseType || 'arraybuffer', + url, req; + + dfd.fail(function() { + req && req.state() === 'pending' && req.reject(); + }); + + url = self.openUrl(hash); + if (!self.isSameOrigin(url)) { + url = self.openUrl(hash, true); + } + req = self.request({ + data : {cmd : 'get'}, + options : { + url: url, + type: 'get', + cache : true, + dataType : 'binary', + responseType : type, + processData: false + } + }) + .fail(function() { + dfd.reject(); + }) + .done(function(data) { + dfd.resolve(data); + }); + + return dfd; + }; + + this.getMimetype = function(name, orgMime) { + var mime = orgMime, + ext, m; + m = (name + '').match(/\.([^.]+)$/); + if (m && (ext = m[1])) { + if (!extToMimeTable) { + extToMimeTable = self.arrayFlip(self.mimeTypes); + } + if (!(mime = extToMimeTable[ext.toLowerCase()])) { + mime = orgMime; + } + } + return mime; + }; + + /** + * Supported check hash algorisms + * + * @type Array + */ + self.hashCheckers = []; + + /** + * Closure of getContentsHashes() + */ + (function(self) { + var hashLibs = { + check : true + }, + md5Calc = function(arr) { + var spark = new hashLibs.SparkMD5.ArrayBuffer(), + job; + + job = self.asyncJob(function(buf) { + spark.append(buf); + }, arr).done(function() { + job._md5 = spark.end(); + }); + + return job; + }, + shaCalc = function(arr, length) { + var sha, job; + + try { + sha = new hashLibs.jsSHA('SHA' + (length.substr(0, 1) === '3'? length : ('-' + length)), 'ARRAYBUFFER'); + job = self.asyncJob(function(buf) { + sha.update(buf); + }, arr).done(function() { + job._sha = sha.getHash('HEX'); + }); + } catch(e) { + job = $.Deferred.reject(); + } + + return job; + }; + + // make fm.hashCheckers + if (self.options.cdns.sparkmd5) { + self.hashCheckers.push('md5'); + } + if (self.options.cdns.jssha) { + self.hashCheckers = self.hashCheckers.concat(['sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512', 'shake128', 'shake256']); + } + + /** + * Gets the contents hashes. + * + * @param String target target file.hash + * @param Object needHashes need hash lib names + * @return Object hashes with lib name as key + */ + self.getContentsHashes = function(target, needHashes) { + var dfd = $.Deferred(), + needs = self.arrayFlip(needHashes || ['md5'], true), + libs = [], + jobs = [], + res = {}, + req; + + dfd.fail(function() { + req && req.reject(); + }); + + if (hashLibs.check) { + + delete hashLibs.check; + + // load SparkMD5 + var libsmd5 = $.Deferred(); + if (window.ArrayBuffer && self.options.cdns.sparkmd5) { + libs.push(libsmd5); + self.loadScript([self.options.cdns.sparkmd5], + function(res) { + var SparkMD5 = res || window.SparkMD5; + window.SparkMD5 && delete window.SparkMD5; + libsmd5.resolve(); + if (SparkMD5) { + hashLibs.SparkMD5 = SparkMD5; + } + }, + { + tryRequire: true, + error: function() { + libsmd5.reject(); + } + } + ); + } + + // load jsSha + var libssha = $.Deferred(); + if (window.ArrayBuffer && self.options.cdns.jssha) { + libs.push(libssha); + self.loadScript([self.options.cdns.jssha], + function(res) { + var jsSHA = res || window.jsSHA; + window.jsSHA && delete window.jsSHA; + libssha.resolve(); + if (jsSHA) { + hashLibs.jsSHA = jsSHA; + } + }, + { + tryRequire: true, + error: function() { + libssha.reject(); + } + } + ); + } + } + + $.when.apply(null, libs).always(function() { + if (Object.keys(hashLibs).length) { + req = self.getContents(target).done(function(arrayBuffer) { + var arr = (arrayBuffer instanceof ArrayBuffer && arrayBuffer.byteLength > 0)? self.sliceArrayBuffer(arrayBuffer, 1048576) : false, + i; + + if (needs.md5 && hashLibs.SparkMD5) { + jobs.push(function() { + var job = md5Calc(arr).done(function() { + var f; + res.md5 = job._md5; + if (f = self.file(target)) { + f.md5 = job._md5; + } + dfd.notify(res); + }); + dfd.fail(function() { + job.reject(); + }); + return job; + }); + } + if (hashLibs.jsSHA) { + $.each(['1', '224', '256', '384', '512', '3-224', '3-256', '3-384', '3-512', 'ke128', 'ke256'], function(i, v) { + if (needs['sha' + v]) { + jobs.push(function() { + var job = shaCalc(arr, v).done(function() { + var f; + res['sha' + v] = job._sha; + if (f = self.file(target)) { + f['sha' + v] = job._sha; + } + dfd.notify(res); + }); + return job; + }); + } + }); + } + if (jobs.length) { + self.sequence(jobs).always(function() { + dfd.resolve(res); + }); + } else { + dfd.reject(); + } + }).fail(function() { + dfd.reject(); + }); + } else { + dfd.reject(); + } + }); + + return dfd; + }; + })(this); + + /** + * Parse error value to display + * + * @param Mixed error + * @return Mixed parsed error + */ + this.parseError = function(error) { + var arg = error; + if ($.isPlainObject(arg)) { + arg = arg.error; + } + return arg; + }; + + /** + * Alias for this.trigger('error', {error : 'message'}) + * + * @param String error message + * @return elFinder + **/ + this.error = function() { + var arg = arguments[0], + opts = arguments[1] || null, + err; + if (arguments.length == 1 && typeof(arg) === 'function') { + return self.bind('error', arg); + } else { + err = this.parseError(arg); + return (err === true || !err)? this : self.trigger('error', {error: err, opts : opts}); + } + }; + + // create bind/trigger aliases for build-in events + $.each(events, function(i, name) { + self[name] = function() { + var arg = arguments[0]; + return arguments.length == 1 && typeof(arg) == 'function' + ? self.bind(name, arg) + : self.trigger(name, $.isPlainObject(arg) ? arg : {}); + }; + }); + + // bind core event handlers + this + .enable(function() { + if (!enabled && self.api && self.visible() && self.ui.overlay.is(':hidden') && ! node.children('.elfinder-dialog.' + self.res('class', 'editing') + ':visible').length) { + enabled = true; + document.activeElement && document.activeElement.blur(); + node.removeClass('elfinder-disabled'); + } + }) + .disable(function() { + prevEnabled = enabled; + enabled = false; + node.addClass('elfinder-disabled'); + }) + .open(function() { + selected = []; + }) + .select(function(e) { + var cnt = 0, + unselects = []; + selected = $.grep(e.data.selected || e.data.value|| [], function(hash) { + if (unselects.length || (self.maxTargets && ++cnt > self.maxTargets)) { + unselects.push(hash); + return false; + } else { + return files[hash] ? true : false; + } + }); + if (unselects.length) { + self.trigger('unselectfiles', {files: unselects, inselect: true}); + self.toast({mode: 'warning', msg: self.i18n(['errMaxTargets', self.maxTargets])}); + } + }) + .error(function(e) { + var opts = { + cssClass : 'elfinder-dialog-error', + title : self.i18n('error'), + resizable : false, + destroyOnClose : true, + buttons : {} + }, + node = self.getUI(), + cnt = node.children('.elfinder-dialog-error').length, + last, counter; + + if (cnt < self.options.maxErrorDialogs) { + opts.buttons[self.i18n(self.i18n('btnClose'))] = function() { $(this).elfinderdialog('close'); }; + + if (e.data.opts && $.isPlainObject(e.data.opts)) { + Object.assign(opts, e.data.opts); + } + + self.dialog(''+self.i18n(e.data.error), opts); + } else { + last = node.children('.elfinder-dialog-error:last').children('.ui-dialog-content:first'); + counter = last.children('.elfinder-error-counter'); + if (counter.length) { + counter.data('cnt', parseInt(counter.data('cnt')) + 1).html(self.i18n(['moreErrors', counter.data('cnt')])); + } else { + counter = $(''+ self.i18n(['moreErrors', 1]) +'').data('cnt', 1); + last.append('
    ', counter); + } + } + }) + .bind('tmb', function(e) { + $.each(e.data.images||[], function(hash, tmb) { + if (files[hash]) { + files[hash].tmb = tmb; + } + }); + }) + .bind('searchstart', function(e) { + Object.assign(self.searchStatus, e.data); + self.searchStatus.state = 1; + }) + .bind('search', function(e) { + self.searchStatus.state = 2; + }) + .bind('searchend', function() { + self.searchStatus.state = 0; + self.searchStatus.ininc = false; + self.searchStatus.mixed = false; + }) + .bind('canMakeEmptyFile', function(e) { + var data = e.data, + obj = {}; + if (data && Array.isArray(data.mimes)) { + if (!data.unshift) { + obj = self.mimesCanMakeEmpty; + } + $.each(data.mimes, function() { + if (!obj[this]) { + obj[this] = self.mimeTypes[this]; + } + }); + if (data.unshift) { + self.mimesCanMakeEmpty = Object.assign(obj, self.mimesCanMakeEmpty); + } + } + }) + .bind('themechange', function() { + requestAnimationFrame(function() { + self.trigger('uiresize'); + }); + }) + ; + + // We listen and emit a sound on delete according to option + if (true === this.options.sound) { + this.bind('playsound', function(e) { + var play = beeper.canPlayType && beeper.canPlayType('audio/wav; codecs="1"'), + file = e.data && e.data.soundFile; + + play && file && play != '' && play != 'no' && $(beeper).html('')[0].play(); + }); + } + + // bind external event handlers + $.each(this.options.handlers, function(event, callback) { + self.bind(event, callback); + }); + + /** + * History object. Store visited folders + * + * @type Object + **/ + this.history = new this.history(this); + + /** + * Root hashed + * + * @type Object + */ + this.roots = {}; + + /** + * leaf roots + * + * @type Object + */ + this.leafRoots = {}; + + this.volumeExpires = {}; + + /** + * Loaded commands + * + * @type Object + **/ + this._commands = {}; + + if (!Array.isArray(this.options.commands)) { + this.options.commands = []; + } + + if ($.inArray('*', this.options.commands) !== -1) { + this.options.commands = Object.keys(this.commands); + } + + /** + * UI command map of cwd volume ( That volume driver option `uiCmdMap` ) + * + * @type Object + **/ + this.commandMap = {}; + + /** + * cwd options of each volume + * key: volumeid + * val: options object + * + * @type Object + */ + this.volOptions = {}; + + /** + * Has volOptions data + * + * @type Boolean + */ + this.hasVolOptions = false; + + /** + * Hash of trash holders + * key: trash folder hash + * val: source volume hash + * + * @type Object + */ + this.trashes = {}; + + /** + * cwd options of each folder/file + * key: hash + * val: options object + * + * @type Object + */ + this.optionsByHashes = {}; + + /** + * UI Auto Hide Functions + * Each auto hide function mast be call to `fm.trigger('uiautohide')` at end of process + * + * @type Array + **/ + this.uiAutoHide = []; + + // trigger `uiautohide` + this.one('open', function() { + if (self.uiAutoHide.length) { + setTimeout(function() { + self.trigger('uiautohide'); + }, 500); + } + }); + + // Auto Hide Functions sequential processing start + this.bind('uiautohide', function() { + if (self.uiAutoHide.length) { + self.uiAutoHide.shift()(); + } + }); + + if (this.options.width) { + width = this.options.width; + } + + if (this.options.height) { + height = this.options.height; + } + + if (this.options.heightBase) { + heightBase = $(this.options.heightBase); + } + + if (this.options.soundPath) { + soundPath = this.options.soundPath.replace(/\/+$/, '') + '/'; + } else { + soundPath = this.baseUrl + soundPath; + } + + self.one('opendone', function() { + var tm; + // attach events to document + $(document) + // disable elfinder on click outside elfinder + .on('click.'+namespace, function(e) { enabled && ! self.options.enableAlways && !$(e.target).closest(node).length && self.disable(); }) + // exec shortcuts + .on(keydown+' '+keypress+' '+keyup+' '+mousedown, execShortcut); + + // attach events to window + self.options.useBrowserHistory && $(window) + .on('popstate.' + namespace, function(ev) { + var state = ev.originalEvent.state || {}, + hasThash = state.thash? true : false, + dialog = node.find('.elfinder-frontmost:visible'), + input = node.find('.elfinder-navbar-dir,.elfinder-cwd-filename').find('input,textarea'), + onOpen, toast; + if (!hasThash) { + state = { thash: self.cwd().hash }; + // scroll to elFinder node + $('html,body').animate({ scrollTop: node.offset().top }); + } + if (dialog.length || input.length) { + history.pushState(state, null, location.pathname + location.search + '#elf_' + state.thash); + if (dialog.length) { + if (!dialog.hasClass(self.res('class', 'preventback'))) { + if (dialog.hasClass('elfinder-contextmenu')) { + $(document).trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, ctrlKey : false, shiftKey : false, altKey : false, metaKey : false })); + } else if (dialog.hasClass('elfinder-dialog')) { + dialog.elfinderdialog('close'); + } else { + dialog.trigger('close'); + } + } + } else { + input.trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, ctrlKey : false, shiftKey : false, altKey : false, metaKey : false })); + } + } else { + if (hasThash) { + !$.isEmptyObject(self.files()) && self.request({ + data : {cmd : 'open', target : state.thash, onhistory : 1}, + notify : {type : 'open', cnt : 1, hideCnt : true}, + syncOnFail : true + }); + } else { + onOpen = function() { + toast.trigger('click'); + }; + self.one('open', onOpen, true); + toast = self.toast({ + msg: self.i18n('pressAgainToExit'), + onHidden: function() { + self.unbind('open', onOpen); + history.pushState(state, null, location.pathname + location.search + '#elf_' + state.thash); + } + }); + } + } + }); + + $(window).on('resize.' + namespace, function(e){ + if (e.target === this) { + tm && cancelAnimationFrame(tm); + tm = requestAnimationFrame(function() { + var prv = node.data('resizeSize') || {w: 0, h: 0}, + size = {w: Math.round(node.width()), h: Math.round(node.height())}; + node.data('resizeSize', size); + if (size.w !== prv.w || size.h !== prv.h) { + node.trigger('resize'); + self.trigger('resize', {width : size.w, height : size.h}); + } + }); + } + }) + .on('beforeunload.' + namespace,function(e){ + var msg, cnt; + if (!self.pauseUnloadCheck()) { + if (node.is(':visible')) { + if (self.ui.notify.children().length && $.inArray('hasNotifyDialog', self.options.windowCloseConfirm) !== -1) { + msg = self.i18n('ntfsmth'); + } else if (node.find('.'+self.res('class', 'editing')).length && $.inArray('editingFile', self.options.windowCloseConfirm) !== -1) { + msg = self.i18n('editingFile'); + } else if ((cnt = Object.keys(self.selected()).length) && $.inArray('hasSelectedItem', self.options.windowCloseConfirm) !== -1) { + msg = self.i18n('hasSelected', ''+cnt); + } else if ((cnt = Object.keys(self.clipboard()).length) && $.inArray('hasClipboardData', self.options.windowCloseConfirm) !== -1) { + msg = self.i18n('hasClipboard', ''+cnt); + } + if (msg) { + e.returnValue = msg; + return msg; + } + } + self.trigger('unload'); + } + }); + + // bind window onmessage for CORS + $(window).on('message.' + namespace, function(e){ + var res = e.originalEvent || null, + obj, data; + if (res && self.uploadURL.indexOf(res.origin) === 0) { + try { + obj = JSON.parse(res.data); + data = obj.data || null; + if (data) { + if (data.error) { + if (obj.bind) { + self.trigger(obj.bind+'fail', data); + } + self.error(data.error); + } else { + data.warning && self.error(data.warning); + self.updateCache(data); + data.removed && data.removed.length && self.remove(data); + data.added && data.added.length && self.add(data); + data.changed && data.changed.length && self.change(data); + if (obj.bind) { + self.trigger(obj.bind, data); + self.trigger(obj.bind+'done'); + } + data.sync && self.sync(); + } + } + } catch (e) { + self.sync(); + } + } + }); + + // elFinder enable always + if (self.options.enableAlways) { + $(window).on('focus.' + namespace, function(e){ + (e.target === this) && self.enable(); + }); + if (inFrame) { + $(window.top).on('focus.' + namespace, function() { + if (self.enable() && (! parentIframe || parentIframe.is(':visible'))) { + requestAnimationFrame(function() { + $(window).trigger('focus'); + }); + } + }); + } + } else if (inFrame) { + $(window).on('blur.' + namespace, function(e){ + enabled && e.target === this && self.disable(); + }); + } + + // return focus to the window on click (elFInder in the frame) + if (inFrame) { + node.on('click', function(e) { + $(window).trigger('focus'); + }); + } + + // elFinder to enable by mouse over + if (self.options.enableByMouseOver) { + node.on('mouseenter touchstart', function(e) { + (inFrame) && $(window).trigger('focus'); + ! self.enabled() && self.enable(); + }); + } + + // When the browser tab turn to foreground/background + $(window).on('visibilitychange.' + namespace, function(e) { + var background = document.hidden || document.webkitHidden || document.msHidden; + // AutoSync turn On/Off + if (self.options.syncStart) { + self.autoSync(background? 'stop' : void(0)); + } + }); + }); + + // store instance in node + node[0].elfinder = this; + + // auto load language file + dfrdsBeforeBootup.push((function() { + var lang = self.lang, + langJs = self.i18nBaseUrl + 'elfinder.' + lang + '.js', + dfd = $.Deferred().done(function() { + if (self.i18[lang]) { + self.lang = lang; + } + self.trigger('i18load'); + i18n = self.lang === 'en' + ? self.i18['en'] + : $.extend(true, {}, self.i18['en'], self.i18[self.lang]); + }); + + if (!self.i18[lang]) { + self.lang = 'en'; + if (self.hasRequire) { + require([langJs], function() { + dfd.resolve(); + }, function() { + dfd.resolve(); + }); + } else { + self.loadScript([langJs], function() { + dfd.resolve(); + }, { + loadType: 'tag', + error : function() { + dfd.resolve(); + } + }); + } + } else { + dfd.resolve(); + } + return dfd; + })()); + + // elFinder boot up function + bootUp = function() { + var columnNames; + + /** + * i18 messages + * + * @type Object + **/ + self.messages = i18n.messages; + + // check jquery ui + if (!($.fn.selectable && $.fn.draggable && $.fn.droppable && $.fn.resizable && $.fn.button && $.fn.slider)) { + return alert(self.i18n('errJqui')); + } + + // check node + if (!node.length) { + return alert(self.i18n('errNode')); + } + // check connector url + if (!self.options.url) { + return alert(self.i18n('errURL')); + } + + // column key/name map for fm.getColumnName() + columnNames = Object.assign({ + name : self.i18n('name'), + perm : self.i18n('perms'), + date : self.i18n('modify'), + size : self.i18n('size'), + kind : self.i18n('kind'), + modestr : self.i18n('mode'), + modeoct : self.i18n('mode'), + modeboth : self.i18n('mode') + }, self.options.uiOptions.cwd.listView.columnsCustomName); + + /** + * Gets the column name of cwd list view + * + * @param String key The key + * @return String The column name. + */ + self.getColumnName = function(key) { + return columnNames[key] || self.i18n(key); + }; + + /** + * Interface direction + * + * @type String + * @default "ltr" + **/ + self.direction = i18n.direction; + + /** + * Date/time format + * + * @type String + * @default "m.d.Y" + **/ + self.dateFormat = self.options.dateFormat || i18n.dateFormat; + + /** + * Date format like "Yesterday 10:20:12" + * + * @type String + * @default "{day} {time}" + **/ + self.fancyFormat = self.options.fancyDateFormat || i18n.fancyDateFormat; + + /** + * Date format for if upload file has not original unique name + * e.g. Clipboard image data, Image data taken with iOS + * + * @type String + * @default "ymd-His" + **/ + self.nonameDateFormat = (self.options.nonameDateFormat || i18n.nonameDateFormat).replace(/[\/\\]/g, '_'); + + /** + * Css classes + * + * @type String + **/ + self.cssClass = 'ui-helper-reset ui-helper-clearfix ui-widget ui-widget-content ui-corner-all elfinder elfinder-' + +(self.direction == 'rtl' ? 'rtl' : 'ltr') + +(self.UA.Touch? (' elfinder-touch' + (self.options.resizable ? ' touch-punch' : '')) : '') + +(self.UA.Mobile? ' elfinder-mobile' : '') + +(self.UA.iOS? ' elfinder-ios' : '') + +' '+self.options.cssClass; + + // prepare node + node.addClass(self.cssClass) + .on(mousedown, function() { + !enabled && self.enable(); + }); + + // draggable closure + (function() { + var ltr, wzRect, wzBottom, wzBottom2, nodeStyle, + keyEvt = keydown + 'draggable' + ' keyup.' + namespace + 'draggable'; + + /** + * Base draggable options + * + * @type Object + **/ + self.draggable = { + appendTo : node, + addClasses : false, + distance : 4, + revert : true, + refreshPositions : false, + cursor : 'crosshair', + cursorAt : {left : 50, top : 47}, + scroll : false, + start : function(e, ui) { + var helper = ui.helper, + targets = $.grep(helper.data('files')||[], function(h) { + if (h) { + remember[h] = true; + return true; + } + return false; + }), + locked = false, + cnt, h; + + // fix node size + nodeStyle = node.attr('style'); + node.width(node.width()).height(node.height()); + + // set var for drag() + ltr = (self.direction === 'ltr'); + wzRect = self.getUI('workzone').data('rectangle'); + wzBottom = wzRect.top + wzRect.height; + wzBottom2 = wzBottom - self.getUI('navdock').outerHeight(true); + + self.draggingUiHelper = helper; + cnt = targets.length; + while (cnt--) { + h = targets[cnt]; + if (files[h].locked) { + locked = true; + helper.data('locked', true); + break; + } + } + !locked && self.trigger('lockfiles', {files : targets}); + + helper.data('autoScrTm', setInterval(function() { + if (helper.data('autoScr')) { + self.autoScroll[helper.data('autoScr')](helper.data('autoScrVal')); + } + }, 50)); + }, + drag : function(e, ui) { + var helper = ui.helper, + autoScr, autoUp, bottom; + + if ((autoUp = wzRect.top > e.pageY) || wzBottom2 < e.pageY) { + if (wzRect.cwdEdge > e.pageX) { + autoScr = (ltr? 'navbar' : 'cwd') + (autoUp? 'Up' : 'Down'); + } else { + autoScr = (ltr? 'cwd' : 'navbar') + (autoUp? 'Up' : 'Down'); + } + if (!autoUp) { + if (autoScr.substr(0, 3) === 'cwd') { + if (wzBottom < e.pageY) { + bottom = wzBottom; + } else { + autoScr = null; + } + } else { + bottom = wzBottom2; + } + } + if (autoScr) { + helper.data('autoScr', autoScr); + helper.data('autoScrVal', Math.pow((autoUp? wzRect.top - e.pageY : e.pageY - bottom), 1.3)); + } + } + if (! autoScr) { + if (helper.data('autoScr')) { + helper.data('refreshPositions', 1).data('autoScr', null); + } + } + if (helper.data('refreshPositions') && $(this).elfUiWidgetInstance('draggable')) { + if (helper.data('refreshPositions') > 0) { + $(this).draggable('option', { refreshPositions : true, elfRefresh : true }); + helper.data('refreshPositions', -1); + } else { + $(this).draggable('option', { refreshPositions : false, elfRefresh : false }); + helper.data('refreshPositions', null); + } + } + }, + stop : function(e, ui) { + var helper = ui.helper, + files; + + $(document).off(keyEvt); + $(this).elfUiWidgetInstance('draggable') && $(this).draggable('option', { refreshPositions : false }); + self.draggingUiHelper = null; + self.trigger('focus').trigger('dragstop'); + if (! helper.data('droped')) { + files = $.grep(helper.data('files')||[], function(h) { return h? true : false ;}); + self.trigger('unlockfiles', {files : files}); + self.trigger('selectfiles', {files : self.selected()}); + } + self.enable(); + + // restore node style + node.attr('style', nodeStyle); + + helper.data('autoScrTm') && clearInterval(helper.data('autoScrTm')); + }, + helper : function(e, ui) { + var element = this.id ? $(this) : $(this).parents('[id]:first'), + helper = $('
    '), + icon = function(f) { + var mime = f.mime, i, tmb = self.tmb(f); + i = '
    '; + if (tmb) { + i = $(i).addClass(tmb.className).css('background-image', "url('"+tmb.url+"')").get(0).outerHTML; + } else if (f.icon) { + i = $(i).css(self.getIconStyle(f, true)).get(0).outerHTML; + } + if (f.csscls) { + i = '
    ' + i + '
    '; + } + return i; + }, + hashes, l, ctr; + + self.draggingUiHelper && self.draggingUiHelper.stop(true, true); + + self.trigger('dragstart', {target : element[0], originalEvent : e}, true); + + hashes = element.hasClass(self.res('class', 'cwdfile')) + ? self.selected() + : [self.navId2Hash(element.attr('id'))]; + + helper.append(icon(files[hashes[0]])).data('files', hashes).data('locked', false).data('droped', false).data('namespace', namespace).data('dropover', 0); + + if ((l = hashes.length) > 1) { + helper.append(icon(files[hashes[l-1]]) + ''+l+''); + } + + $(document).on(keyEvt, function(e){ + var chk = (e.shiftKey||e.ctrlKey||e.metaKey); + if (ctr !== chk) { + ctr = chk; + if (helper.is(':visible') && helper.data('dropover') && ! helper.data('droped')) { + helper.toggleClass('elfinder-drag-helper-plus', helper.data('locked')? true : ctr); + self.trigger(ctr? 'unlockfiles' : 'lockfiles', {files : hashes, helper: helper}); + } + } + }); + + return helper; + } + }; + })(); + + // in getFileCallback set - change default actions on double click/enter/ctrl+enter + if (self.commands.getfile) { + if (typeof(self.options.getFileCallback) == 'function') { + self.bind('dblclick', function(e) { + e.preventDefault(); + self.exec('getfile').fail(function() { + self.exec('open', e.data && e.data.file? [ e.data.file ]: void(0)); + }); + }); + self.shortcut({ + pattern : 'enter', + description : self.i18n('cmdgetfile'), + callback : function() { self.exec('getfile').fail(function() { self.exec(self.OS == 'mac' ? 'rename' : 'open'); }); } + }) + .shortcut({ + pattern : 'ctrl+enter', + description : self.i18n(self.OS == 'mac' ? 'cmdrename' : 'cmdopen'), + callback : function() { self.exec(self.OS == 'mac' ? 'rename' : 'open'); } + }); + } else { + self.options.getFileCallback = null; + } + } + + // load commands + $.each(self.commands, function(name, cmd) { + var proto = Object.assign({}, cmd.prototype), + extendsCmd, opts; + if ($.isFunction(cmd) && !self._commands[name] && (cmd.prototype.forceLoad || $.inArray(name, self.options.commands) !== -1)) { + extendsCmd = cmd.prototype.extendsCmd || ''; + if (extendsCmd) { + if ($.isFunction(self.commands[extendsCmd])) { + cmd.prototype = Object.assign({}, base, new self.commands[extendsCmd](), cmd.prototype); + } else { + return true; + } + } else { + cmd.prototype = Object.assign({}, base, cmd.prototype); + } + self._commands[name] = new cmd(); + cmd.prototype = proto; + opts = self.options.commandsOptions[name] || {}; + if (extendsCmd && self.options.commandsOptions[extendsCmd]) { + opts = $.extend(true, {}, self.options.commandsOptions[extendsCmd], opts); + } + self._commands[name].setup(name, opts); + // setup linked commands + if (self._commands[name].linkedCmds.length) { + $.each(self._commands[name].linkedCmds, function(i, n) { + var lcmd = self.commands[n]; + if ($.isFunction(lcmd) && !self._commands[n]) { + lcmd.prototype = base; + self._commands[n] = new lcmd(); + self._commands[n].setup(n, self.options.commandsOptions[n]||{}); + } + }); + } + } + }); + + /** + * UI nodes + * + * @type Object + **/ + self.ui = { + // container for nav panel and current folder container + workzone : $('
    ').appendTo(node).elfinderworkzone(self), + // container for folders tree / places + navbar : $('
    ').appendTo(node).elfindernavbar(self, self.options.uiOptions.navbar || {}), + // container for for preview etc at below the navbar + navdock : $('
    ').appendTo(node).elfindernavdock(self, self.options.uiOptions.navdock || {}), + // contextmenu + contextmenu : $('
    ').appendTo(node).elfindercontextmenu(self), + // overlay + overlay : $('
    ').appendTo(node).elfinderoverlay({ + show : function() { self.disable(); }, + hide : function() { prevEnabled && self.enable(); } + }), + // current folder container + cwd : $('
    ').appendTo(node).elfindercwd(self, self.options.uiOptions.cwd || {}), + // notification dialog window + notify : self.dialog('', { + cssClass : 'elfinder-dialog-notify', + position : self.options.notifyDialog.position, + absolute : true, + resizable : false, + autoOpen : false, + closeOnEscape : false, + title : ' ', + width : self.options.notifyDialog.width? parseInt(self.options.notifyDialog.width) : null, + minHeight : null + }), + statusbar : $('
    ').hide().appendTo(node), + toast : $('
    ').appendTo(node), + bottomtray : $('
    ').appendTo(node) + }; + + self.trigger('uiready'); + + // load required ui + $.each(self.options.ui || [], function(i, ui) { + var name = 'elfinder'+ui, + opts = self.options.uiOptions[ui] || {}; + + if (!self.ui[ui] && $.fn[name]) { + // regist to self.ui before make instance + self.ui[ui] = $('<'+(opts.tag || 'div')+'/>').appendTo(node); + self.ui[ui][name](self, opts); + } + }); + + // update size + self.resize(width, height); + + // make node resizable + if (self.options.resizable) { + node.resizable({ + resize : function(e, ui) { + self.resize(ui.size.width, ui.size.height); + }, + handles : 'se', + minWidth : 300, + minHeight : 200 + }); + if (self.UA.Touch) { + node.addClass('touch-punch'); + } + } + + (function() { + var navbar = self.getUI('navbar'), + cwd = self.getUI('cwd').parent(); + + self.autoScroll = { + navbarUp : function(v) { + navbar.scrollTop(Math.max(0, navbar.scrollTop() - v)); + }, + navbarDown : function(v) { + navbar.scrollTop(navbar.scrollTop() + v); + }, + cwdUp : function(v) { + cwd.scrollTop(Math.max(0, cwd.scrollTop() - v)); + }, + cwdDown : function(v) { + cwd.scrollTop(cwd.scrollTop() + v); + } + }; + })(); + + // Swipe on the touch devices to show/hide of toolbar or navbar + if (self.UA.Touch) { + (function() { + var lastX, lastY, nodeOffset, nodeWidth, nodeTop, navbarW, toolbarH, + navbar = self.getUI('navbar'), + toolbar = self.getUI('toolbar'), + moveEv = 'touchmove.stopscroll', + moveTm, + moveUpOn = function(e) { + var touches = e.originalEvent.touches || [{}], + y = touches[0].pageY || null; + if (!lastY || y < lastY) { + e.preventDefault(); + moveTm && clearTimeout(moveTm); + } + }, + moveDownOn = function(e) { + e.preventDefault(); + moveTm && clearTimeout(moveTm); + }, + moveOff = function() { + moveTm = setTimeout(function() { + node.off(moveEv); + }, 100); + }, + handleW, handleH = 50; + + navbar = navbar.children().length? navbar : null; + toolbar = toolbar.length? toolbar : null; + node.on('touchstart touchmove touchend', function(e) { + if (e.type === 'touchend') { + lastX = false; + lastY = false; + moveOff(); + return; + } + + var touches = e.originalEvent.touches || [{}], + x = touches[0].pageX || null, + y = touches[0].pageY || null, + ltr = (self.direction === 'ltr'), + navbarMode, treeWidth, swipeX, moveX, toolbarT, mode; + + if (x === null || y === null || (e.type === 'touchstart' && touches.length > 1)) { + return; + } + + if (e.type === 'touchstart') { + nodeOffset = node.offset(); + nodeWidth = node.width(); + if (navbar) { + lastX = false; + if (navbar.is(':hidden')) { + if (! handleW) { + handleW = Math.max(50, nodeWidth / 10); + } + if ((ltr? (x - nodeOffset.left) : (nodeWidth + nodeOffset.left - x)) < handleW) { + lastX = x; + } + } else if (! e.originalEvent._preventSwipeX) { + navbarW = navbar.width(); + if (ltr) { + swipeX = (x < nodeOffset.left + navbarW); + } else { + swipeX = (x > nodeOffset.left + nodeWidth - navbarW); + } + if (swipeX) { + handleW = Math.max(50, nodeWidth / 10); + lastX = x; + } else { + lastX = false; + } + } + } + if (toolbar) { + lastY = false; + if (! e.originalEvent._preventSwipeY) { + toolbarH = toolbar.height(); + nodeTop = nodeOffset.top; + if (y - nodeTop < (toolbar.is(':hidden')? handleH : (toolbarH + 30))) { + lastY = y; + node.on(moveEv, toolbar.is(':hidden')? moveDownOn: moveUpOn); + } + } + } + } else { + if (navbar && lastX !== false) { + navbarMode = (ltr? (lastX > x) : (lastX < x))? 'navhide' : 'navshow'; + moveX = Math.abs(lastX - x); + if (navbarMode === 'navhide' && moveX > navbarW * 0.6 + || (moveX > (navbarMode === 'navhide'? navbarW / 3 : 45) + && (navbarMode === 'navshow' + || (ltr? x < nodeOffset.left + 20 : x > nodeOffset.left + nodeWidth - 20) + )) + ) { + self.getUI('navbar').trigger(navbarMode, {handleW: handleW}); + lastX = false; + } + } + if (toolbar && lastY !== false ) { + toolbarT = toolbar.offset().top; + if (Math.abs(lastY - y) > Math.min(45, toolbarH / 3)) { + mode = (lastY > y)? 'slideUp' : 'slideDown'; + if (mode === 'slideDown' || toolbarT + 20 > y) { + if (toolbar.is(mode === 'slideDown' ? ':hidden' : ':visible')) { + toolbar.stop(true, true).trigger('toggle', {duration: 100, handleH: handleH}); + } + lastY = false; + } + } + } + } + }); + })(); + } + + if (self.dragUpload) { + // add event listener for HTML5 DnD upload + (function() { + var isin = function(e) { + return (e.target.nodeName !== 'TEXTAREA' && e.target.nodeName !== 'INPUT' && $(e.target).closest('div.ui-dialog-content').length === 0); + }, + ent = 'native-drag-enter', + disable = 'native-drag-disable', + c = 'class', + navdir = self.res(c, 'navdir'), + droppable = self.res(c, 'droppable'), + dropover = self.res(c, 'adroppable'), + arrow = self.res(c, 'navarrow'), + clDropActive = self.res(c, 'adroppable'), + wz = self.getUI('workzone'), + ltr = (self.direction === 'ltr'), + clearTm = function() { + autoScrTm && cancelAnimationFrame(autoScrTm); + autoScrTm = null; + }, + wzRect, autoScrFn, autoScrTm; + + node.on('dragenter', function(e) { + clearTm(); + if (isin(e)) { + e.preventDefault(); + e.stopPropagation(); + wzRect = wz.data('rectangle'); + } + }) + .on('dragleave', function(e) { + clearTm(); + if (isin(e)) { + e.preventDefault(); + e.stopPropagation(); + } + }) + .on('dragover', function(e) { + var autoUp; + if (isin(e)) { + e.preventDefault(); + e.stopPropagation(); + e.originalEvent.dataTransfer.dropEffect = 'none'; + if (! autoScrTm) { + autoScrTm = requestAnimationFrame(function() { + var wzBottom = wzRect.top + wzRect.height, + wzBottom2 = wzBottom - self.getUI('navdock').outerHeight(true), + fn; + if ((autoUp = e.pageY < wzRect.top) || e.pageY > wzBottom2 ) { + if (wzRect.cwdEdge > e.pageX) { + fn = (ltr? 'navbar' : 'cwd') + (autoUp? 'Up' : 'Down'); + } else { + fn = (ltr? 'cwd' : 'navbar') + (autoUp? 'Up' : 'Down'); + } + if (!autoUp) { + if (fn.substr(0, 3) === 'cwd') { + if (wzBottom < e.pageY) { + wzBottom2 = wzBottom; + } else { + fn = ''; + } + } + } + fn && self.autoScroll[fn](Math.pow((autoUp? wzRect.top - e.pageY : e.pageY - wzBottom2), 1.3)); + } + autoScrTm = null; + }); + } + } else { + clearTm(); + } + }) + .on('drop', function(e) { + clearTm(); + if (isin(e)) { + e.stopPropagation(); + e.preventDefault(); + } + }); + + node.on('dragenter', '.native-droppable', function(e){ + if (e.originalEvent.dataTransfer) { + var $elm = $(e.currentTarget), + id = e.currentTarget.id || null, + cwd = null, + elfFrom; + if (!id) { // target is cwd + cwd = self.cwd(); + $elm.data(disable, false); + try { + $.each(e.originalEvent.dataTransfer.types, function(i, v){ + if (v.substr(0, 13) === 'elfinderfrom:') { + elfFrom = v.substr(13).toLowerCase(); + } + }); + } catch(e) {} + } + if (!cwd || (cwd.write && (!elfFrom || elfFrom !== (window.location.href + cwd.hash).toLowerCase()))) { + e.preventDefault(); + e.stopPropagation(); + $elm.data(ent, true); + $elm.addClass(clDropActive); + } else { + $elm.data(disable, true); + } + } + }) + .on('dragleave', '.native-droppable', function(e){ + if (e.originalEvent.dataTransfer) { + var $elm = $(e.currentTarget); + e.preventDefault(); + e.stopPropagation(); + if ($elm.data(ent)) { + $elm.data(ent, false); + } else { + $elm.removeClass(clDropActive); + } + } + }) + .on('dragover', '.native-droppable', function(e){ + if (e.originalEvent.dataTransfer) { + var $elm = $(e.currentTarget); + e.preventDefault(); + e.stopPropagation(); + e.originalEvent.dataTransfer.dropEffect = $elm.data(disable)? 'none' : 'copy'; + $elm.data(ent, false); + } + }) + .on('drop', '.native-droppable', function(e){ + if (e.originalEvent && e.originalEvent.dataTransfer) { + var $elm = $(e.currentTarget), + id; + e.preventDefault(); + e.stopPropagation(); + $elm.removeClass(clDropActive); + if (e.currentTarget.id) { + id = $elm.hasClass(navdir)? self.navId2Hash(e.currentTarget.id) : self.cwdId2Hash(e.currentTarget.id); + } else { + id = self.cwd().hash; + } + e.originalEvent._target = id; + self.exec('upload', {dropEvt: e.originalEvent, target: id}, void 0, id); + } + }); + })(); + } + + // trigger event cssloaded if cssAutoLoad disabled + if (self.cssloaded === false) { + self.cssloaded = true; + self.trigger('cssloaded'); + } + + // calculate elFinder node z-index + self.zIndexCalc(); + + // send initial request and start to pray >_< + self.trigger('init') + .request({ + data : {cmd : 'open', target : self.startDir(), init : 1, tree : 1}, + preventDone : true, + notify : {type : 'open', cnt : 1, hideCnt : true}, + freeze : true + }) + .fail(function() { + self.trigger('fail').disable().lastDir(''); + listeners = {}; + shortcuts = {}; + $(document).add(node).off('.'+namespace); + self.trigger = function() { }; + }) + .done(function(data) { + var trashDisable = function(th) { + var src = self.file(self.trashes[th]), + d = self.options.debug, + error; + + if (src && src.volumeid) { + delete self.volOptions[src.volumeid].trashHash; + } + self.trashes[th] = false; + self.debug('backend-error', 'Trash hash "'+th+'" was not found or not writable.'); + }, + toChkTh = {}; + + // regist rawStringDecoder + if (self.options.rawStringDecoder) { + self.registRawStringDecoder(self.options.rawStringDecoder); + } + + // re-calculate elFinder node z-index + self.zIndexCalc(); + + self.load().debug('api', self.api); + // update ui's size after init + node.trigger('resize'); + // initial open + open(data); + self.trigger('open', data, false); + self.trigger('opendone'); + + if (inFrame && self.options.enableAlways) { + $(window).trigger('focus'); + } + + // check self.trashes + $.each(self.trashes, function(th) { + var dir = self.file(th), + src; + if (! dir) { + toChkTh[th] = true; + } else if (dir.mime !== 'directory' || ! dir.write) { + trashDisable(th); + } + }); + if (Object.keys(toChkTh).length) { + self.request({ + data : {cmd : 'info', targets : Object.keys(toChkTh)}, + preventDefault : true + }).done(function(data) { + if (data && data.files) { + $.each(data.files, function(i, dir) { + if (dir.mime === 'directory' && dir.write) { + delete toChkTh[dir.hash]; + } + }); + } + }).always(function() { + $.each(toChkTh, trashDisable); + }); + } + // to enable / disable + self[self.options.enableAlways? 'enable' : 'disable'](); + }); + + // self.timeEnd('load'); + // End of bootUp() + }; + + // call bootCallback function with elFinder instance, extraObject - { dfrdsBeforeBootup: dfrdsBeforeBootup } + if (bootCallback && typeof bootCallback === 'function') { + self.bootCallback = bootCallback; + bootCallback.call(node.get(0), self, { dfrdsBeforeBootup: dfrdsBeforeBootup }); + } + + // call dfrdsBeforeBootup functions then boot up elFinder + $.when.apply(null, dfrdsBeforeBootup).done(function() { + bootUp(); + }).fail(function(error) { + self.error(error); + }); +}; + +//register elFinder to global scope +if (typeof toGlobal === 'undefined' || toGlobal) { + window.elFinder = elFinder; +} + +/** + * Prototype + * + * @type Object + */ +elFinder.prototype = { + + uniqueid : 0, + + res : function(type, id) { + return this.resources[type] && this.resources[type][id]; + }, + + /** + * User os. Required to bind native shortcuts for open/rename + * + * @type String + **/ + OS : navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : navigator.userAgent.indexOf('Win') !== -1 ? 'win' : 'other', + + /** + * User browser UA. + * jQuery.browser: version deprecated: 1.3, removed: 1.9 + * + * @type Object + **/ + UA : (function(){ + var self = this, + webkit = !document.unqueID && !window.opera && !window.sidebar && window.localStorage && 'WebkitAppearance' in document.documentElement.style, + chrome = webkit && window.chrome, + /*setRotated = function() { + var a = ((screen && screen.orientation && screen.orientation.angle) || window.orientation || 0) + 0; + if (a === -90) { + a = 270; + } + UA.Angle = a; + UA.Rotated = a % 180 === 0? false : true; + },*/ + UA = { + // Browser IE <= IE 6 + ltIE6 : typeof window.addEventListener == "undefined" && typeof document.documentElement.style.maxHeight == "undefined", + // Browser IE <= IE 7 + ltIE7 : typeof window.addEventListener == "undefined" && typeof document.querySelectorAll == "undefined", + // Browser IE <= IE 8 + ltIE8 : typeof window.addEventListener == "undefined" && typeof document.getElementsByClassName == "undefined", + // Browser IE <= IE 9 + ltIE9 : document.uniqueID && document.documentMode <= 9, + // Browser IE <= IE 10 + ltIE10 : document.uniqueID && document.documentMode <= 10, + // Browser IE >= IE 11 + gtIE11 : document.uniqueID && document.documentMode >= 11, + IE : document.uniqueID, + Firefox : window.sidebar, + Opera : window.opera, + Webkit : webkit, + Chrome : chrome, + Edge : (chrome && window.msCredentials)? true : false, + Safari : webkit && !window.chrome, + Mobile : typeof window.orientation != "undefined", + Touch : typeof window.ontouchstart != "undefined", + iOS : navigator.platform.match(/^iP(?:[ao]d|hone)/), + Fullscreen : (typeof (document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen) !== 'undefined'), + Angle : 0, + Rotated : false, + CSS : (function() { + var aStyle = document.createElement('a').style, + pStyle = document.createElement('p').style, + css; + css = 'position:sticky;position:-webkit-sticky;'; + css += 'width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:max-content;'; + aStyle.cssText = css; + return { + positionSticky : aStyle.position.indexOf('sticky')!==-1, + widthMaxContent : aStyle.width.indexOf('max-content')!==-1, + flex : typeof pStyle.flex !== 'undefined' + }; + })() + }; + return UA; + })(), + + /** + * Has RequireJS? + * + * @type Boolean + */ + hasRequire : (typeof define === 'function' && define.amd), + + /** + * Current request command + * + * @type String + */ + currentReqCmd : '', + + /** + * Current keyboard state + * + * @type Object + */ + keyState : {}, + + /** + * Internationalization object + * + * @type Object + */ + i18 : { + en : { + translator : '', + language : 'English', + direction : 'ltr', + dateFormat : 'd.m.Y H:i', + fancyDateFormat : '$1 H:i', + nonameDateFormat : 'ymd-His', + messages : {} + }, + months : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + monthsShort : ['msJan', 'msFeb', 'msMar', 'msApr', 'msMay', 'msJun', 'msJul', 'msAug', 'msSep', 'msOct', 'msNov', 'msDec'], + + days : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + daysShort : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + }, + + /** + * File mimetype to kind mapping + * + * @type Object + */ + kinds : { + 'unknown' : 'Unknown', + 'directory' : 'Folder', + 'group' : 'Selects', + 'symlink' : 'Alias', + 'symlink-broken' : 'AliasBroken', + 'application/x-empty' : 'TextPlain', + 'application/postscript' : 'Postscript', + 'application/vnd.ms-office' : 'MsOffice', + 'application/msword' : 'MsWord', + 'application/vnd.ms-word' : 'MsWord', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : 'MsWord', + 'application/vnd.ms-word.document.macroEnabled.12' : 'MsWord', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' : 'MsWord', + 'application/vnd.ms-word.template.macroEnabled.12' : 'MsWord', + 'application/vnd.ms-excel' : 'MsExcel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'MsExcel', + 'application/vnd.ms-excel.sheet.macroEnabled.12' : 'MsExcel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' : 'MsExcel', + 'application/vnd.ms-excel.template.macroEnabled.12' : 'MsExcel', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' : 'MsExcel', + 'application/vnd.ms-excel.addin.macroEnabled.12' : 'MsExcel', + 'application/vnd.ms-powerpoint' : 'MsPP', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' : 'MsPP', + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' : 'MsPP', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' : 'MsPP', + 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' : 'MsPP', + 'application/vnd.openxmlformats-officedocument.presentationml.template' : 'MsPP', + 'application/vnd.ms-powerpoint.template.macroEnabled.12' : 'MsPP', + 'application/vnd.ms-powerpoint.addin.macroEnabled.12' : 'MsPP', + 'application/vnd.openxmlformats-officedocument.presentationml.slide' : 'MsPP', + 'application/vnd.ms-powerpoint.slide.macroEnabled.12' : 'MsPP', + 'application/pdf' : 'PDF', + 'application/xml' : 'XML', + 'application/vnd.oasis.opendocument.text' : 'OO', + 'application/vnd.oasis.opendocument.text-template' : 'OO', + 'application/vnd.oasis.opendocument.text-web' : 'OO', + 'application/vnd.oasis.opendocument.text-master' : 'OO', + 'application/vnd.oasis.opendocument.graphics' : 'OO', + 'application/vnd.oasis.opendocument.graphics-template' : 'OO', + 'application/vnd.oasis.opendocument.presentation' : 'OO', + 'application/vnd.oasis.opendocument.presentation-template' : 'OO', + 'application/vnd.oasis.opendocument.spreadsheet' : 'OO', + 'application/vnd.oasis.opendocument.spreadsheet-template' : 'OO', + 'application/vnd.oasis.opendocument.chart' : 'OO', + 'application/vnd.oasis.opendocument.formula' : 'OO', + 'application/vnd.oasis.opendocument.database' : 'OO', + 'application/vnd.oasis.opendocument.image' : 'OO', + 'application/vnd.openofficeorg.extension' : 'OO', + 'application/x-shockwave-flash' : 'AppFlash', + 'application/flash-video' : 'Flash video', + 'application/x-bittorrent' : 'Torrent', + 'application/javascript' : 'JS', + 'application/rtf' : 'RTF', + 'application/rtfd' : 'RTF', + 'application/x-font-ttf' : 'TTF', + 'application/x-font-otf' : 'OTF', + 'application/x-rpm' : 'RPM', + 'application/x-web-config' : 'TextPlain', + 'application/xhtml+xml' : 'HTML', + 'application/docbook+xml' : 'DOCBOOK', + 'application/x-awk' : 'AWK', + 'application/x-gzip' : 'GZIP', + 'application/x-bzip2' : 'BZIP', + 'application/x-xz' : 'XZ', + 'application/zip' : 'ZIP', + 'application/x-zip' : 'ZIP', + 'application/x-rar' : 'RAR', + 'application/x-tar' : 'TAR', + 'application/x-7z-compressed' : '7z', + 'application/x-jar' : 'JAR', + 'text/plain' : 'TextPlain', + 'text/x-php' : 'PHP', + 'text/html' : 'HTML', + 'text/javascript' : 'JS', + 'text/css' : 'CSS', + 'text/rtf' : 'RTF', + 'text/rtfd' : 'RTF', + 'text/x-c' : 'C', + 'text/x-csrc' : 'C', + 'text/x-chdr' : 'CHeader', + 'text/x-c++' : 'CPP', + 'text/x-c++src' : 'CPP', + 'text/x-c++hdr' : 'CPPHeader', + 'text/x-shellscript' : 'Shell', + 'application/x-csh' : 'Shell', + 'text/x-python' : 'Python', + 'text/x-java' : 'Java', + 'text/x-java-source' : 'Java', + 'text/x-ruby' : 'Ruby', + 'text/x-perl' : 'Perl', + 'text/x-sql' : 'SQL', + 'text/xml' : 'XML', + 'text/x-comma-separated-values' : 'CSV', + 'text/x-markdown' : 'Markdown', + 'image/x-ms-bmp' : 'BMP', + 'image/jpeg' : 'JPEG', + 'image/gif' : 'GIF', + 'image/png' : 'PNG', + 'image/tiff' : 'TIFF', + 'image/x-targa' : 'TGA', + 'image/vnd.adobe.photoshop' : 'PSD', + 'image/xbm' : 'XBITMAP', + 'image/pxm' : 'PXM', + 'audio/mpeg' : 'AudioMPEG', + 'audio/midi' : 'AudioMIDI', + 'audio/ogg' : 'AudioOGG', + 'audio/mp4' : 'AudioMPEG4', + 'audio/x-m4a' : 'AudioMPEG4', + 'audio/wav' : 'AudioWAV', + 'audio/x-mp3-playlist' : 'AudioPlaylist', + 'video/x-dv' : 'VideoDV', + 'video/mp4' : 'VideoMPEG4', + 'video/mpeg' : 'VideoMPEG', + 'video/x-msvideo' : 'VideoAVI', + 'video/quicktime' : 'VideoMOV', + 'video/x-ms-wmv' : 'VideoWM', + 'video/x-flv' : 'VideoFlash', + 'video/x-matroska' : 'VideoMKV', + 'video/ogg' : 'VideoOGG' + }, + + /** + * File mimetype to file extention mapping + * + * @type Object + * @see elFinder.mimetypes.js + */ + mimeTypes : {}, + + /** + * Ajax request data validation rules + * + * @type Object + */ + rules : { + defaults : function(data) { + if (!data + || (data.added && !Array.isArray(data.added)) + || (data.removed && !Array.isArray(data.removed)) + || (data.changed && !Array.isArray(data.changed))) { + return false; + } + return true; + }, + open : function(data) { return data && data.cwd && data.files && $.isPlainObject(data.cwd) && Array.isArray(data.files); }, + tree : function(data) { return data && data.tree && Array.isArray(data.tree); }, + parents : function(data) { return data && data.tree && Array.isArray(data.tree); }, + tmb : function(data) { return data && data.images && ($.isPlainObject(data.images) || Array.isArray(data.images)); }, + upload : function(data) { return data && ($.isPlainObject(data.added) || Array.isArray(data.added));}, + search : function(data) { return data && data.files && Array.isArray(data.files); } + }, + + /** + * Commands costructors + * + * @type Object + */ + commands : {}, + + /** + * Commands to add the item (space delimited) + * + * @type String + */ + cmdsToAdd : 'archive duplicate extract mkdir mkfile paste rm upload', + + parseUploadData : function(text) { + var self = this, + data; + + if (!$.trim(text)) { + return {error : ['errResponse', 'errDataEmpty']}; + } + + try { + data = JSON.parse(text); + } catch (e) { + return {error : ['errResponse', 'errDataNotJSON']}; + } + + data = self.normalize(data); + if (!self.validResponse('upload', data)) { + return {error : (data.norError || ['errResponse'])}; + } + data.removed = $.merge((data.removed || []), $.map(data.added || [], function(f) { return self.file(f.hash)? f.hash : null; })); + return data; + + }, + + iframeCnt : 0, + + uploads : { + // xhr muiti uploading flag + xhrUploading: false, + + // Timer of request fail to sync + failSyncTm: null, + + // current chunkfail requesting chunk + chunkfailReq: {}, + + // check file/dir exists + checkExists: function(files, target, fm, isDir) { + var dfrd = $.Deferred(), + names, renames = [], hashes = {}, chkFiles = [], + cancel = function() { + var i = files.length; + while (--i > -1) { + files[i]._remove = true; + } + }, + resolve = function() { + dfrd.resolve(renames, hashes); + }, + check = function() { + var existed = [], exists = [], i, c, + pathStr = target !== fm.cwd().hash? fm.path(target, true) + fm.option('separator', target) : '', + confirm = function(ndx) { + var last = ndx == exists.length-1, + opts = { + cssClass : 'elfinder-confirm-upload', + title : fm.i18n('cmdupload'), + text : ['errExists', pathStr + exists[ndx].name, 'confirmRepl'], + all : !last, + accept : { + label : 'btnYes', + callback : function(all) { + !last && !all + ? confirm(++ndx) + : resolve(); + } + }, + reject : { + label : 'btnNo', + callback : function(all) { + var i; + + if (all) { + i = exists.length; + while (ndx < i--) { + files[exists[i].i]._remove = true; + } + } else { + files[exists[ndx].i]._remove = true; + } + + !last && !all + ? confirm(++ndx) + : resolve(); + } + }, + cancel : { + label : 'btnCancel', + callback : function() { + cancel(); + resolve(); + } + }, + buttons : [ + { + label : 'btnBackup', + cssClass : 'elfinder-confirm-btn-backup', + callback : function(all) { + var i; + if (all) { + i = exists.length; + while (ndx < i--) { + renames.push(exists[i].name); + } + } else { + renames.push(exists[ndx].name); + } + !last && !all + ? confirm(++ndx) + : resolve(); + } + } + ] + }; + + if (!isDir) { + opts.buttons.push({ + label : 'btnRename' + (last? '' : 'All'), + cssClass : 'elfinder-confirm-btn-rename', + callback : function() { + renames = null; + resolve(); + } + }); + } + if (fm.iframeCnt > 0) { + delete opts.reject; + } + fm.confirm(opts); + }; + + if (! fm.file(target).read) { + // for dropbox type + resolve(); + return; + } + + names = $.map(files, function(file, i) { return file.name && (!fm.UA.iOS || file.name !== 'image.jpg')? {i: i, name: file.name} : null ;}); + + fm.request({ + data : {cmd : 'ls', target : target, intersect : $.map(names, function(item) { return item.name;})}, + notify : {type : 'preupload', cnt : 1, hideCnt : true}, + preventDefault : true + }) + .done(function(data) { + var existedArr, cwdItems; + if (data) { + if (data.error) { + cancel(); + } else { + if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) { + if (data.list) { + if (Array.isArray(data.list)) { + existed = data.list || []; + } else { + existedArr = []; + existed = $.map(data.list, function(n) { + if (typeof n === 'string') { + return n; + } else { + // support to >=2.1.11 plugin Normalizer, Sanitizer + existedArr = existedArr.concat(n); + return false; + } + }); + if (existedArr.length) { + existed = existed.concat(existedArr); + } + hashes = data.list; + } + exists = $.grep(names, function(name){ + return $.inArray(name.name, existed) !== -1 ? true : false ; + }); + if (exists.length && existed.length && target == fm.cwd().hash) { + cwdItems = $.map(fm.files(target), function(file) { return file.name; } ); + if ($.grep(existed, function(n) { + return $.inArray(n, cwdItems) === -1? true : false; + }).length){ + fm.sync(); + } + } + } + } + } + } + if (exists.length > 0) { + confirm(0); + } else { + resolve(); + } + }) + .fail(function(error) { + cancel(); + resolve(); + error && fm.error(error); + }); + }; + if (fm.api >= 2.1 && typeof files[0] == 'object') { + check(); + } else { + resolve(); + } + return dfrd; + }, + + // check droped contents + checkFile : function(data, fm, target) { + if (!!data.checked || data.type == 'files') { + return data.files; + } else if (data.type == 'data') { + var dfrd = $.Deferred(), + scanDfd = $.Deferred(), + files = [], + paths = [], + dirctorys = [], + processing = 0, + items, + mkdirs = [], + cancel = false, + toArray = function(list) { + return Array.prototype.slice.call(list || [], 0); + }, + doScan = function(items) { + var entry, readEntries, + excludes = fm.options.folderUploadExclude[fm.OS] || null, + length = items.length, + check = function() { + if (--processing < 1 && scanDfd.state() === 'pending') { + scanDfd.resolve(); + } + }, + pushItem = function(file) { + if (! excludes || ! file.name.match(excludes)) { + paths.push(entry.fullPath || ''); + files.push(file); + } + check(); + }, + readEntries = function(dirReader) { + var entries = [], + read = function() { + dirReader.readEntries(function(results) { + if (cancel || !results.length) { + for (var i = 0; i < entries.length; i++) { + if (cancel) { + scanDfd.reject(); + break; + } + doScan([entries[i]]); + } + check(); + } else { + entries = entries.concat(toArray(results)); + read(); + } + }, check); + }; + read(); + }; + + processing++; + for (var i = 0; i < length; i++) { + if (cancel) { + scanDfd.reject(); + break; + } + entry = items[i]; + if (entry) { + if (entry.isFile) { + processing++; + entry.file(pushItem, check); + } else if (entry.isDirectory) { + if (fm.api >= 2.1) { + processing++; + mkdirs.push(entry.fullPath); + readEntries(entry.createReader()); // Start reading dirs. + } + } + } + } + check(); + return scanDfd; + }, hasDirs; + + items = $.map(data.files.items, function(item){ + return item.getAsEntry? item.getAsEntry() : item.webkitGetAsEntry(); + }); + $.each(items, function(i, item) { + if (item.isDirectory) { + hasDirs = true; + return false; + } + }); + if (items.length > 0) { + fm.uploads.checkExists(items, target, fm, hasDirs).done(function(renames, hashes){ + var dfds = []; + if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) { + if (renames === null) { + data.overwrite = 0; + renames = []; + } + items = $.grep(items, function(item){ + var i, bak, hash, dfd, hi; + if (item.isDirectory && renames.length) { + i = $.inArray(item.name, renames); + if (i !== -1) { + renames.splice(i, 1); + bak = fm.uniqueName(item.name + fm.options.backupSuffix , null, ''); + $.each(hashes, function(h, name) { + if (item.name == name) { + hash = h; + return false; + } + }); + if (! hash) { + hash = fm.fileByName(item.name, target).hash; + } + fm.lockfiles({files : [hash]}); + dfd = fm.request({ + data : {cmd : 'rename', target : hash, name : bak}, + notify : {type : 'rename', cnt : 1} + }) + .fail(function() { + item._remove = true; + fm.sync(); + }) + .always(function() { + fm.unlockfiles({files : [hash]}); + }); + dfds.push(dfd); + } + } + return !item._remove? true : false; + }); + } + $.when.apply($, dfds).done(function(){ + var notifyto, msg, + id = +new Date(); + + if (items.length > 0) { + msg = fm.escape(items[0].name); + if (items.length > 1) { + msg += ' ... ' + items.length + fm.i18n('items'); + } + notifyto = setTimeout(function() { + fm.notify({ + type : 'readdir', + id : id, + cnt : 1, + hideCnt: true, + msg : fm.i18n('ntfreaddir') + ' (' + msg + ')', + cancel: function() { + cancel = true; + } + }); + }, fm.options.notifyDelay); + doScan(items).done(function() { + notifyto && clearTimeout(notifyto); + fm.notify({type : 'readdir', id: id, cnt : -1}); + if (cancel) { + dfrd.reject(); + } else { + dfrd.resolve([files, paths, renames, hashes, mkdirs]); + } + }).fail(function() { + dfrd.reject(); + }); + } else { + dfrd.reject(); + } + }); + }); + return dfrd.promise(); + } else { + return dfrd.reject(); + } + } else { + var ret = []; + var check = []; + var str = data.files[0]; + if (data.type == 'html') { + var tmp = $("").append($.parseHTML(str.replace(/ src=/ig, ' _elfsrc='))), + atag; + $('img[_elfsrc]', tmp).each(function(){ + var url, purl, + self = $(this), + pa = self.closest('a'); + if (pa && pa.attr('href') && pa.attr('href').match(/\.(?:jpe?g|gif|bmp|png)/i)) { + purl = pa.attr('href'); + } + url = self.attr('_elfsrc'); + if (url) { + if (purl) { + $.inArray(purl, ret) == -1 && ret.push(purl); + $.inArray(url, check) == -1 && check.push(url); + } else { + $.inArray(url, ret) == -1 && ret.push(url); + } + } + // Probably it's clipboard data + if (ret.length === 1 && ret[0].match(/^data:image\/png/)) { + data.clipdata = true; + } + }); + atag = $('a[href]', tmp); + atag.each(function(){ + var text, loc, + parseUrl = function(url) { + var a = document.createElement('a'); + a.href = url; + return a; + }; + if (text = $(this).text()) { + loc = parseUrl($(this).attr('href')); + if (loc.href && loc.href.match(/^(?:ht|f)tp/i) && (atag.length === 1 || ! loc.pathname.match(/(?:\.html?|\/[^\/.]*)$/i) || $.trim(text).match(/\.[a-z0-9-]{1,10}$/i))) { + if ($.inArray(loc.href, ret) == -1 && $.inArray(loc.href, check) == -1) ret.push(loc.href); + } + } + }); + } else { + var regex, m, url; + regex = /((?:ht|f)tps?:\/\/[-_.!~*\'()a-z0-9;/?:\@&=+\$,%#\*\[\]]+)/ig; + while (m = regex.exec(str)) { + url = m[1].replace(/&/g, '&'); + if ($.inArray(url, ret) == -1) ret.push(url); + } + } + return ret; + } + }, + + // upload transport using XMLHttpRequest + xhr : function(data, fm) { + var self = fm ? fm : this, + node = self.getUI(), + xhr = new XMLHttpRequest(), + notifyto = null, notifyto2 = null, + dataChecked = data.checked, + isDataType = (data.isDataType || data.type == 'data'), + target = (data.target || self.cwd().hash), + dropEvt = (data.dropEvt || null), + extraData = data.extraData || null, + chunkEnable = (self.option('uploadMaxConn', target) != -1), + multiMax = Math.min(5, Math.max(1, self.option('uploadMaxConn', target))), + retryWait = 10000, // 10 sec + retryMax = 30, // 10 sec * 30 = 300 secs (Max 5 mins) + retry = 0, + getFile = function(files) { + var dfd = $.Deferred(), + file; + if (files.promise) { + files.always(function(f) { + dfd.resolve(Array.isArray(f) && f.length? (isDataType? f[0][0] : f[0]) : {}); + }); + } else { + dfd.resolve(files.length? (isDataType? files[0][0] : files[0]) : {}); + } + return dfd; + }, + dfrd = $.Deferred() + .fail(function(err) { + var error = self.parseError(err), + userAbort; + if (error === 'userabort') { + userAbort = true; + error = void 0; + } + if (files && (self.uploads.xhrUploading || userAbort)) { + // send request om fail + getFile(files).done(function(file) { + if (!userAbort) { + triggerError(error, file); + } + if (! file._cid) { + // send sync request + self.uploads.failSyncTm && clearTimeout(self.uploads.failSyncTm); + self.uploads.failSyncTm = setTimeout(function() { + self.sync(target); + }, 1000); + } else if (! self.uploads.chunkfailReq[file._cid]) { + // send chunkfail request + self.uploads.chunkfailReq[file._cid] = true; + setTimeout(function() { + fm.request({ + data : { + cmd: 'upload', + target: target, + chunk: file._chunk, + cid: file._cid, + upload: ['chunkfail'], + mimes: 'chunkfail' + }, + options : { + type: 'post', + url: self.uploadURL + }, + preventDefault: true + }).always(function() { + delete self.uploads.chunkfailReq[file._chunk]; + }); + }, 1000); + } + }); + } else { + triggerError(error); + } + !userAbort && self.sync(); + self.uploads.xhrUploading = false; + files = null; + }) + .done(function(data) { + self.uploads.xhrUploading = false; + files = null; + if (data) { + self.currentReqCmd = 'upload'; + data.warning && triggerError(data.warning); + self.updateCache(data); + data.removed && data.removed.length && self.remove(data); + data.added && data.added.length && self.add(data); + data.changed && data.changed.length && self.change(data); + self.trigger('upload', data, false); + self.trigger('uploaddone'); + if (data.toasts && Array.isArray(data.toasts)) { + $.each(data.toasts, function() { + this.msg && self.toast(this); + }); + } + data.sync && self.sync(); + data.debug && fm.debug('backend-debug', data); + } + }) + .always(function() { + self.abortXHR(xhr); + // unregist fnAbort function + node.off('uploadabort', fnAbort); + $(window).off('unload', fnAbort); + notifyto && clearTimeout(notifyto); + notifyto2 && clearTimeout(notifyto2); + dataChecked && !data.multiupload && checkNotify() && self.notify({type : 'upload', cnt : -cnt, progress : 0, size : 0}); + chunkMerge && notifyElm.children('.elfinder-notify-chunkmerge').length && self.notify({type : 'chunkmerge', cnt : -1}); + }), + formData = new FormData(), + files = data.input ? data.input.files : self.uploads.checkFile(data, self, target), + cnt = data.checked? (isDataType? files[0].length : files.length) : files.length, + loaded = 0, + prev = 0, + filesize = 0, + notify = false, + notifyElm = self.ui.notify, + cancelBtn = true, + abort = false, + checkNotify = function() { + if (!notify && (ntfUpload = notifyElm.children('.elfinder-notify-upload')).length) { + notify = true; + } + return notify; + }, + fnAbort = function(e, error) { + abort = true; + self.abortXHR(xhr, { quiet: true, abort: true }); + dfrd.reject(error); + if (checkNotify()) { + self.notify({type : 'upload', cnt : ntfUpload.data('cnt') * -1, progress : 0, size : 0}); + } + }, + cancelToggle = function(show) { + ntfUpload.children('.elfinder-notify-cancel')[show? 'show':'hide'](); + }, + startNotify = function(size) { + if (!size) size = filesize; + return setTimeout(function() { + notify = true; + self.notify({type : 'upload', cnt : cnt, progress : loaded - prev, size : size, + cancel: function() { + node.trigger('uploadabort', 'userabort'); + } + }); + ntfUpload = notifyElm.children('.elfinder-notify-upload'); + prev = loaded; + if (data.multiupload) { + cancelBtn && cancelToggle(true); + } else { + cancelToggle(cancelBtn && loaded < size); + } + }, self.options.notifyDelay); + }, + doRetry = function() { + if (retry++ <= retryMax) { + if (checkNotify() && prev) { + self.notify({type : 'upload', cnt : 0, progress : 0, size : prev}); + } + self.abortXHR(xhr, { quiet: true }); + prev = loaded = 0; + setTimeout(function() { + var reqId; + if (! abort) { + xhr.open('POST', self.uploadURL, true); + if (self.api >= 2.1029) { + reqId = (+ new Date()).toString(16) + Math.floor(1000 * Math.random()).toString(16); + (typeof formData['delete'] === 'function') && formData['delete']('reqid'); + formData.append('reqid', reqId); + xhr._requestId = reqId; + } + xhr.send(formData); + } + }, retryWait); + } else { + node.trigger('uploadabort', ['errAbort', 'errTimeout']); + } + }, + progress = function() { + var node; + if (notify) { + dfrd.notifyWith(ntfUpload, [{ + cnt: ntfUpload.data('cnt'), + progress: ntfUpload.data('progress'), + total: ntfUpload.data('total') + }]); + } + }, + triggerError = function(err, file, unite) { + err && self.trigger('xhruploadfail', { error: err, file: file }); + if (unite) { + if (err) { + if (errCnt < self.options.maxErrorDialogs) { + if (Array.isArray(err)) { + errors = errors.concat(err); + } else { + errors.push(err); + } + } + errCnt++; + } + } else { + if (err) { + self.error(err); + } else { + if (errors.length) { + if (errCnt >= self.options.maxErrorDialogs) { + errors = errors.concat('moreErrors', errCnt - self.options.maxErrorDialogs); + } + self.error(errors); + } + errors = []; + errCnt = 0; + } + } + }, + errors = [], + errCnt = 0, + renames = (data.renames || null), + hashes = (data.hashes || null), + chunkMerge = false, + ntfUpload = $(); + + // regist fnAbort function + node.one('uploadabort', fnAbort); + $(window).one('unload.' + fm.namespace, fnAbort); + + !chunkMerge && (prev = loaded); + + if (!isDataType && !cnt) { + return dfrd.reject(['errUploadNoFiles']); + } + + xhr.addEventListener('error', function() { + if (xhr.status == 0) { + if (abort) { + dfrd.reject(); + } else { + // ff bug while send zero sized file + // for safari - send directory + if (!isDataType && data.files && $.grep(data.files, function(f){return ! f.type && f.size === (self.UA.Safari? 1802 : 0)? true : false;}).length) { + dfrd.reject(['errAbort', 'errFolderUpload']); + } else if (data.input && $.grep(data.input.files, function(f){return ! f.type && f.size === (self.UA.Safari? 1802 : 0)? true : false;}).length) { + dfrd.reject(['errUploadNoFiles']); + } else { + doRetry(); + } + } + } else { + node.trigger('uploadabort', 'errConnect'); + } + }, false); + + xhr.addEventListener('load', function(e) { + var status = xhr.status, res, curr = 0, error = '', errData, errObj; + + if (status >= 400) { + if (status > 500) { + error = 'errResponse'; + } else { + error = ['errResponse', 'errServerError']; + } + } else { + if (!xhr.responseText) { + error = ['errResponse', 'errDataEmpty']; + } + } + + if (error) { + node.trigger('uploadabort'); + getFile(files || {}).done(function(file) { + return dfrd.reject(file._cid? null : error); + }); + } + + loaded = filesize; + + if (checkNotify() && (curr = loaded - prev)) { + self.notify({type : 'upload', cnt : 0, progress : curr, size : 0}); + progress(); + } + + res = self.parseUploadData(xhr.responseText); + + // chunked upload commit + if (res._chunkmerged) { + formData = new FormData(); + var _file = [{_chunkmerged: res._chunkmerged, _name: res._name, _mtime: res._mtime}]; + chunkMerge = true; + node.off('uploadabort', fnAbort); + notifyto2 = setTimeout(function() { + self.notify({type : 'chunkmerge', cnt : 1}); + }, self.options.notifyDelay); + isDataType? send(_file, files[1]) : send(_file); + return; + } + + res._multiupload = data.multiupload? true : false; + if (res.error) { + errData = { + cmd: 'upload', + err: res, + xhr: xhr, + rc: xhr.status + }; + self.trigger('uploadfail', res); + // trigger "requestError" event + self.trigger('requestError', errData); + if (errData._getEvent && errData._getEvent().isDefaultPrevented()) { + res.error = ''; + } + if (res._chunkfailure || res._multiupload) { + abort = true; + self.uploads.xhrUploading = false; + notifyto && clearTimeout(notifyto); + if (ntfUpload.length) { + self.notify({type : 'upload', cnt : -cnt, progress : 0, size : 0}); + dfrd.reject(res); + } else { + // for multi connection + dfrd.reject(); + } + } else { + dfrd.reject(res); + } + } else { + dfrd.resolve(res); + } + }, false); + + xhr.upload.addEventListener('loadstart', function(e) { + if (!chunkMerge && e.lengthComputable) { + loaded = e.loaded; + retry && (loaded = 0); + filesize = e.total; + if (!loaded) { + loaded = parseInt(filesize * 0.05); + } + if (checkNotify()) { + self.notify({type : 'upload', cnt : 0, progress : loaded - prev, size : data.multiupload? 0 : filesize}); + prev = loaded; + progress(); + } + } + }, false); + + xhr.upload.addEventListener('progress', function(e) { + var curr; + + if (e.lengthComputable && !chunkMerge && xhr.readyState < 2) { + + loaded = e.loaded; + + // to avoid strange bug in safari (not in chrome) with drag&drop. + // bug: macos finder opened in any folder, + // reset safari cache (option+command+e), reload elfinder page, + // drop file from finder + // on first attempt request starts (progress callback called ones) but never ends. + // any next drop - successfull. + if (!data.checked && loaded > 0 && !notifyto) { + notifyto = startNotify(xhr._totalSize - loaded); + } + + if (!filesize) { + filesize = e.total; + if (!loaded) { + loaded = parseInt(filesize * 0.05); + } + } + + curr = loaded - prev; + if (checkNotify() && (curr/e.total) >= 0.05) { + self.notify({type : 'upload', cnt : 0, progress : curr, size : 0}); + prev = loaded; + progress(); + } + + if (! data.multiupload && loaded >= filesize) { + cancelBtn = false; + cancelToggle(false); + } + } + }, false); + + var send = function(files, paths){ + var size = 0, + fcnt = 1, + sfiles = [], + c = 0, + total = cnt, + maxFileSize, + totalSize = 0, + chunked = [], + chunkID = new Date().getTime().toString().substr(-9), // for take care of the 32bit backend system + BYTES_PER_CHUNK = Math.min((fm.uplMaxSize? fm.uplMaxSize : 2097152) - 8190, fm.options.uploadMaxChunkSize), // uplMaxSize margin 8kb or options.uploadMaxChunkSize + blobSlice = chunkEnable? false : '', + blobSize, blobMtime, blobName, i, start, end, chunks, blob, chunk, added, done, last, failChunk, + multi = function(files, num){ + var sfiles = [], cid, sfilesLen = 0, cancelChk; + if (!abort) { + while(files.length && sfiles.length < num) { + sfiles.push(files.shift()); + } + sfilesLen = sfiles.length; + if (sfilesLen) { + cancelChk = sfilesLen; + for (var i=0; i < sfilesLen; i++) { + if (abort) { + break; + } + cid = isDataType? (sfiles[i][0][0]._cid || null) : (sfiles[i][0]._cid || null); + if (!!failChunk[cid]) { + last--; + continue; + } + fm.exec('upload', { + type: data.type, + isDataType: isDataType, + files: sfiles[i], + checked: true, + target: target, + dropEvt: dropEvt, + renames: renames, + hashes: hashes, + multiupload: true, + overwrite: data.overwrite === 0? 0 : void 0, + clipdata: data.clipdata + }, void 0, target) + .fail(function(error) { + if (error && error === 'No such command') { + abort = true; + fm.error(['errUpload', 'errPerm']); + } + if (cid) { + failChunk[cid] = true; + } + }) + .always(function(e) { + if (e && e.added) added = $.merge(added, e.added); + if (last <= ++done) { + fm.trigger('multiupload', {added: added}); + notifyto && clearTimeout(notifyto); + if (checkNotify()) { + self.notify({type : 'upload', cnt : -cnt, progress : 0, size : 0}); + } + } + if (files.length) { + multi(files, 1); // Next one + } else { + if (--cancelChk <= 1) { + cancelBtn = false; + cancelToggle(false); + } + } + }); + } + } + } + if (sfiles.length < 1 || abort) { + if (abort) { + notifyto && clearTimeout(notifyto); + if (cid) { + failChunk[cid] = true; + } + dfrd.reject(); + } else { + dfrd.resolve(); + self.uploads.xhrUploading = false; + } + } + }, + check = function(){ + if (!self.uploads.xhrUploading) { + self.uploads.xhrUploading = true; + multi(sfiles, multiMax); // Max connection: 3 + } else { + setTimeout(check, 100); + } + }, + reqId, err; + + if (! dataChecked && (isDataType || data.type == 'files')) { + if (! (maxFileSize = fm.option('uploadMaxSize', target))) { + maxFileSize = 0; + } + for (i=0; i < files.length; i++) { + try { + blob = files[i]; + blobSize = blob.size; + if (blobSlice === false) { + blobSlice = ''; + if (self.api >= 2.1) { + if ('slice' in blob) { + blobSlice = 'slice'; + } else if ('mozSlice' in blob) { + blobSlice = 'mozSlice'; + } else if ('webkitSlice' in blob) { + blobSlice = 'webkitSlice'; + } + } + } + } catch(e) { + cnt--; + total--; + continue; + } + + // file size check + if ((maxFileSize && blobSize > maxFileSize) || (!blobSlice && fm.uplMaxSize && blobSize > fm.uplMaxSize)) { + triggerError(['errUploadFile', blob.name, 'errUploadFileSize'], blob, true); + cnt--; + total--; + continue; + } + + // file mime check + if (blob.type && ! self.uploadMimeCheck(blob.type, target)) { + triggerError(['errUploadFile', blob.name, 'errUploadMime', '(' + blob.type + ')'], blob, true); + cnt--; + total--; + continue; + } + + if (blobSlice && blobSize > BYTES_PER_CHUNK) { + start = 0; + end = BYTES_PER_CHUNK; + chunks = -1; + total = Math.floor((blobSize - 1) / BYTES_PER_CHUNK); + blobMtime = blob.lastModified? Math.round(blob.lastModified/1000) : 0; + blobName = data.clipdata? fm.date(fm.nonameDateFormat) + '.png' : blob.name; + + totalSize += blobSize; + chunked[chunkID] = 0; + while(start < blobSize) { + chunk = blob[blobSlice](start, end); + chunk._chunk = blobName + '.' + (++chunks) + '_' + total + '.part'; + chunk._cid = chunkID; + chunk._range = start + ',' + chunk.size + ',' + blobSize; + chunk._mtime = blobMtime; + chunked[chunkID]++; + + if (size) { + c++; + } + if (typeof sfiles[c] == 'undefined') { + sfiles[c] = []; + if (isDataType) { + sfiles[c][0] = []; + sfiles[c][1] = []; + } + } + size = BYTES_PER_CHUNK; + fcnt = 1; + if (isDataType) { + sfiles[c][0].push(chunk); + sfiles[c][1].push(paths[i]); + } else { + sfiles[c].push(chunk); + } + + start = end; + end = start + BYTES_PER_CHUNK; + } + if (chunk == null) { + triggerError(['errUploadFile', blob.name, 'errUploadFileSize'], blob, true); + cnt--; + total--; + } else { + total += chunks; + size = 0; + fcnt = 1; + c++; + } + continue; + } + if ((fm.uplMaxSize && size + blobSize > fm.uplMaxSize) || fcnt > fm.uplMaxFile) { + size = 0; + fcnt = 1; + c++; + } + if (typeof sfiles[c] == 'undefined') { + sfiles[c] = []; + if (isDataType) { + sfiles[c][0] = []; + sfiles[c][1] = []; + } + } + if (isDataType) { + sfiles[c][0].push(blob); + sfiles[c][1].push(paths[i]); + } else { + sfiles[c].push(blob); + } + size += blobSize; + totalSize += blobSize; + fcnt++; + } + + if (errors.length) { + triggerError(); + } + + if (sfiles.length == 0) { + // no data + data.checked = true; + return false; + } + + if (sfiles.length > 1) { + // multi upload + notifyto = startNotify(totalSize); + added = []; + done = 0; + last = sfiles.length; + failChunk = []; + check(); + return true; + } + + // single upload + if (isDataType) { + files = sfiles[0][0]; + paths = sfiles[0][1]; + } else { + files = sfiles[0]; + } + } + + if (!dataChecked) { + if (!fm.UA.Safari || !data.files) { + notifyto = startNotify(totalSize); + } else { + xhr._totalSize = totalSize; + } + } + + dataChecked = true; + + if (! files.length) { + dfrd.reject(['errUploadNoFiles']); + } + + xhr.open('POST', self.uploadURL, true); + + // set request headers + if (fm.customHeaders) { + $.each(fm.customHeaders, function(key) { + xhr.setRequestHeader(key, this); + }); + } + + // set xhrFields + if (fm.xhrFields) { + $.each(fm.xhrFields, function(key) { + if (key in xhr) { + xhr[key] = this; + } + }); + } + + if (self.api >= 2.1029) { + // request ID + reqId = (+ new Date()).toString(16) + Math.floor(1000 * Math.random()).toString(16); + formData.append('reqid', reqId); + xhr._requestId = reqId; + } + formData.append('cmd', 'upload'); + formData.append(self.newAPI ? 'target' : 'current', target); + if (renames && renames.length) { + $.each(renames, function(i, v) { + formData.append('renames[]', v); + }); + formData.append('suffix', fm.options.backupSuffix); + } + if (hashes) { + $.each(hashes, function(i, v) { + formData.append('hashes['+ i +']', v); + }); + } + $.each(self.customData, function(key, val) { + formData.append(key, val); + }); + $.each(self.options.onlyMimes, function(i, mime) { + formData.append('mimes[]', mime); + }); + + $.each(files, function(i, file) { + var name; + if (file._chunkmerged) { + formData.append('chunk', file._chunkmerged); + formData.append('upload[]', file._name); + formData.append('mtime[]', file._mtime); + data.clipdata && formData.append('overwrite', 0); + } else { + if (file._chunkfail) { + formData.append('upload[]', 'chunkfail'); + formData.append('mimes', 'chunkfail'); + } else { + if (data.clipdata) { + if (!file._chunk) { + data.overwrite = 0; + name = fm.date(fm.nonameDateFormat) + '.png'; + } + } else { + if (file.name) { + name = file.name; + if (fm.UA.iOS) { + if (name.match(/^image\.jpe?g$/i)) { + data.overwrite = 0; + name = fm.date(fm.nonameDateFormat) + '.jpg'; + } else if (name.match(/^capturedvideo\.mov$/i)) { + data.overwrite = 0; + name = fm.date(fm.nonameDateFormat) + '.mov'; + } + } + } + } + name? formData.append('upload[]', file, name) : formData.append('upload[]', file); + } + if (file._chunk) { + formData.append('chunk', file._chunk); + formData.append('cid' , file._cid); + formData.append('range', file._range); + formData.append('mtime[]', file._mtime); + } else { + formData.append('mtime[]', file.lastModified? Math.round(file.lastModified/1000) : 0); + } + } + }); + + if (isDataType) { + $.each(paths, function(i, path) { + formData.append('upload_path[]', path); + }); + } + + if (data.overwrite === 0) { + formData.append('overwrite', 0); + } + + // send int value that which meta key was pressed when dropped as `dropWith` + if (dropEvt) { + formData.append('dropWith', parseInt( + (dropEvt.altKey ? '1' : '0')+ + (dropEvt.ctrlKey ? '1' : '0')+ + (dropEvt.metaKey ? '1' : '0')+ + (dropEvt.shiftKey? '1' : '0'), 2)); + } + + // set extraData on current request + if (extraData) { + $.each(extraData, function(key, val) { + formData.append(key, val); + }); + } + + xhr.send(formData); + + return true; + }; + + if (! isDataType) { + if (files.length > 0) { + if (! data.clipdata && renames == null) { + var mkdirs = [], + paths = [], + excludes = fm.options.folderUploadExclude[fm.OS] || null; + $.each(files, function(i, file) { + var relPath = file.webkitRelativePath || file.relativePath || '', + idx, rootDir; + if (! relPath) { + return false; + } + if (excludes && file.name.match(excludes)) { + file._remove = true; + relPath = void(0); + } else { + // add '/' as prefix to make same to folder uploading with DnD, see #2607 + relPath = '/' + relPath.replace(/\/[^\/]*$/, '').replace(/^\//, ''); + if (relPath && $.inArray(relPath, mkdirs) === -1) { + mkdirs.push(relPath); + // checking the root directory to supports see #2378 + idx = relPath.substr(1).indexOf('/'); + if (idx !== -1 && (rootDir = relPath.substr(0, idx + 1)) && $.inArray(rootDir, mkdirs) === -1) { + mkdirs.unshift(rootDir); + } + } + } + paths.push(relPath); + }); + renames = []; + hashes = {}; + if (mkdirs.length) { + (function() { + var checkDirs = $.map(mkdirs, function(name) { return name.substr(1).indexOf('/') === -1 ? {name: name.substr(1)} : null;}), + cancelDirs = []; + fm.uploads.checkExists(checkDirs, target, fm, true).done( + function(res, res2) { + var dfds = [], dfd, bak, hash; + if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) { + cancelDirs = $.map(checkDirs, function(dir) { return dir._remove? dir.name : null ;} ); + checkDirs = $.grep(checkDirs, function(dir) { return !dir._remove? true : false ;} ); + } + if (cancelDirs.length) { + $.each(paths.concat(), function(i, path) { + if ($.inArray(path, cancelDirs) === 0) { + files[i]._remove = true; + paths[i] = void(0); + } + }); + } + files = $.grep(files, function(file) { return file._remove? false : true; }); + paths = $.grep(paths, function(path) { return path === void 0 ? false : true; }); + if (checkDirs.length) { + dfd = $.Deferred(); + if (res.length) { + $.each(res, function(i, existName) { + // backup + bak = fm.uniqueName(existName + fm.options.backupSuffix , null, ''); + $.each(res2, function(h, name) { + if (res[0] == name) { + hash = h; + return false; + } + }); + if (! hash) { + hash = fm.fileByName(res[0], target).hash; + } + fm.lockfiles({files : [hash]}); + dfds.push( + fm.request({ + data : {cmd : 'rename', target : hash, name : bak}, + notify : {type : 'rename', cnt : 1} + }) + .fail(function(error) { + dfrd.reject(error); + fm.sync(); + }) + .always(function() { + fm.unlockfiles({files : [hash]}); + }) + ); + }); + } else { + dfds.push(null); + } + + $.when.apply($, dfds).done(function() { + // ensure directories + fm.request({ + data : {cmd : 'mkdir', target : target, dirs : mkdirs}, + notify : {type : 'mkdir', cnt : mkdirs.length}, + preventFail: true + }) + .fail(function(error) { + error = error || ['errUnknown']; + if (error[0] === 'errCmdParams') { + multiMax = 1; + } else { + multiMax = 0; + dfrd.reject(error); + } + }) + .done(function(data) { + var rm = false; + if (!data.hashes) { + data.hashes = {}; + } + paths = $.map(paths.concat(), function(p, i) { + if (p === '/') { + return target; + } else { + if (data.hashes[p]) { + return data.hashes[p]; + } else { + rm = true; + files[i]._remove = true; + return null; + } + } + }); + if (rm) { + files = $.grep(files, function(file) { return file._remove? false : true; }); + } + }) + .always(function(data) { + if (multiMax) { + isDataType = true; + if (! send(files, paths)) { + dfrd.reject(); + } + } + }); + }); + } else { + dfrd.reject(); + } + } + ); + })(); + } else { + fm.uploads.checkExists(files, target, fm).done( + function(res, res2){ + if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) { + hashes = res2; + if (res === null) { + data.overwrite = 0; + } else { + renames = res; + } + files = $.grep(files, function(file){return !file._remove? true : false ;}); + } + cnt = files.length; + if (cnt > 0) { + if (! send(files)) { + dfrd.reject(); + } + } else { + dfrd.reject(); + } + } + ); + } + } else { + if (! send(files)) { + dfrd.reject(); + } + } + } else { + dfrd.reject(); + } + } else { + if (dataChecked) { + send(files[0], files[1]); + } else { + files.done(function(result) { // result: [files, paths, renames, hashes, mkdirs] + renames = []; + cnt = result[0].length; + if (cnt) { + if (result[4] && result[4].length) { + // ensure directories + fm.request({ + data : {cmd : 'mkdir', target : target, dirs : result[4]}, + notify : {type : 'mkdir', cnt : result[4].length}, + preventFail: true + }) + .fail(function(error) { + error = error || ['errUnknown']; + if (error[0] === 'errCmdParams') { + multiMax = 1; + } else { + multiMax = 0; + dfrd.reject(error); + } + }) + .done(function(data) { + var rm = false; + if (!data.hashes) { + data.hashes = {}; + } + result[1] = $.map(result[1], function(p, i) { + p = p.replace(/\/[^\/]*$/, ''); + if (p === '') { + return target; + } else { + if (data.hashes[p]) { + return data.hashes[p]; + } else { + rm = true; + result[0][i]._remove = true; + return null; + } + } + }); + if (rm) { + result[0] = $.grep(result[0], function(file) { return file._remove? false : true; }); + } + }) + .always(function(data) { + if (multiMax) { + renames = result[2]; + hashes = result[3]; + send(result[0], result[1]); + } + }); + return; + } else { + result[1] = $.map(result[1], function() { return target; }); + } + renames = result[2]; + hashes = result[3]; + send(result[0], result[1]); + } else { + dfrd.reject(['errUploadNoFiles']); + } + }).fail(function(){ + dfrd.reject(); + }); + } + } + + return dfrd; + }, + + // upload transport using iframe + iframe : function(data, fm) { + var self = fm ? fm : this, + input = data.input? data.input : false, + files = !input ? self.uploads.checkFile(data, self) : false, + dfrd = $.Deferred() + .fail(function(error) { + error && self.error(error); + }), + name = 'iframe-'+fm.namespace+(++self.iframeCnt), + form = $('
    '), + msie = this.UA.IE, + // clear timeouts, close notification dialog, remove form/iframe + onload = function() { + abortto && clearTimeout(abortto); + notifyto && clearTimeout(notifyto); + notify && self.notify({type : 'upload', cnt : -cnt}); + + setTimeout(function() { + msie && $('', + // setup on elFinder bootup + setup : function(opts, fm) { + if (fm.UA.IE || fm.UA.Mobile) { + this.disabled = true; + } + }, + // Initialization of editing node (this: this editors HTML node) + init : function(id, file, dum, fm) { + var orig = 'https://www.photopea.com', + ifm = $(this).hide() + //.css('box-sizing', 'border-box') + .on('load', function() { + //spnr.remove(); + ifm.show(); + }) + .on('error', function() { + spnr.remove(); + ifm.show(); + }), + editor = this.editor, + confObj = editor.confObj, + spnr = $('
    ') + .html('' + fm.i18n('nowLoading') + '') + .appendTo(ifm.parent()), + saveMimes = fm.arrayFlip(confObj.info.canMakeEmpty), + getType = function(mime) { + var ext = getExtention(mime, fm), + extmime = ext2mime[ext]; + + if (!confObj.mimesFlip[extmime]) { + ext = ''; + } else if (ext === 'jpeg') { + ext = 'jpg'; + } + if (!ext || !!saveMimes[ext]) { + ext = 'psd'; + extmime = ext2mime[ext]; + ifm.closest('.ui-dialog').trigger('changeType', { + extention: ext, + mime : extmime, + keepEditor: true + }); + } + return ext; + }, + mime = file.mime, + liveMsg, type, quty; + + if (!confObj.mimesFlip) { + confObj.mimesFlip = fm.arrayFlip(confObj.mimes, true); + } + if (!confObj.liveMsg) { + confObj.liveMsg = function(ifm, spnr, file) { + var url = fm.openUrl(file.hash); + if (!fm.isSameOrigin(url)) { + url = fm.openUrl(file.hash, true); + } + var wnd = ifm.get(0).contentWindow, + phase = 0, + data = null, + dfdIni = $.Deferred().done(function() { + spnr.remove(); + phase = 1; + wnd.postMessage(data, '*'); + }), + dfdGet; + + this.load = function() { + return fm.request({ + data : {cmd : 'get'}, + options : { + url: url, + type: 'get', + cache : true, + dataType : 'binary', + responseType :'arraybuffer', + processData: false + } + }) + .done(function(d) { + data = d; + }); + }; + + this.receive = function(e) { + var ev = e.originalEvent, + state; + if (ev.origin === orig && ev.source === wnd) { + if (ev.data === 'done') { + if (phase === 0) { + dfdIni.resolve(); + } else if (phase === 1) { + phase = 2; + ifm.trigger('contentsloaded'); + } else { + if (dfdGet && dfdGet.state() === 'pending') { + dfdGet.reject('errDataEmpty'); + } + } + } else { + if (dfdGet && dfdGet.state() === 'pending') { + if (typeof ev.data === 'object') { + dfdGet.resolve('data:' + mime + ';base64,' + fm.arrayBufferToBase64(ev.data)); + } else { + dfdGet.reject('errDataEmpty'); + } + } + } + } + }; + + this.getContent = function() { + var type, q; + if (phase > 1) { + dfdGet && dfdGet.state() === 'pending' && dfdGet.reject(); + dfdGet = null; + dfdGet = $.Deferred(); + if (phase === 2) { + phase = 3; + dfdGet.resolve('data:' + mime + ';base64,' + fm.arrayBufferToBase64(data)); + data = null; + return dfdGet; + } + if (ifm.data('mime')) { + mime = ifm.data('mime'); + type = getType(mime); + } + if (q = ifm.data('quality')) { + type += ':' + (q / 100); + } + wnd.postMessage('app.activeDocument.saveToOE("' + type + '")', orig); + return dfdGet; + } + }; + }; + } + + ifm.parent().css('padding', 0); + type = getType(file.mime); + liveMsg = editor.liveMsg = new confObj.liveMsg(ifm, spnr, file); + $(window).on('message.' + fm.namespace, liveMsg.receive); + liveMsg.load().done(function() { + var d = JSON.stringify({ + files : [], + environment : { + lang: fm.lang.replace(/_/g, '-') + } + }); + ifm.attr('src', orig + '/#' + encodeURI(d)); + }).fail(function(err) { + err && fm.error(err); + editor.initFail = true; + }); + + // jpeg quality controls + if (file.mime === 'image/jpeg' || file.mime === 'image/webp') { + ifm.data('quality', fm.storage('jpgQuality') || fm.option('jpgQuality')); + quty = $('') + .attr('min', '1') + .attr('max', '100') + .attr('title', '1 - 100') + .on('change', function() { + var q = quty.val(); + ifm.data('quality', q); + }) + .val(ifm.data('quality')); + $('
    ') + .append( + $('').html(fm.i18n('quality') + ' : '), quty, $('') + ) + .prependTo(ifm.parent().next()); + } + }, + load : function(base) { + var dfd = $.Deferred(), + self = this, + fm = this.fm, + $base = $(base); + if (self.initFail) { + dfd.reject(); + } else { + $base.on('contentsloaded', function() { + dfd.resolve(self.liveMsg); + }); + } + return dfd; + }, + getContent : function() { + return this.editor.liveMsg? this.editor.liveMsg.getContent() : void(0); + }, + save : function(base, liveMsg) { + var $base = $(base), + quality = $base.data('quality'), + hash = $base.data('hash'), + file; + if (typeof quality !== 'undefined') { + this.fm.storage('jpgQuality', quality); + } + if (hash) { + file = this.fm.file(hash); + $base.data('mime', file.mime); + } else { + $base.removeData('mime'); + } + }, + // On dialog closed + close : function(base, liveMsg) { + $(base).attr('src', ''); + liveMsg && $(window).off('message.' + this.fm.namespace, liveMsg.receive); + } + }, + { + // Pixo is cross-platform image editor + info : { + id : 'pixo', + name : 'Pixo Editor', + iconImg : 'img/editor-icons.png 0 -208', + dataScheme: true, + schemeContent: true, + single: true, + canMakeEmpty: false, + integrate: { + title: 'Pixo Editor', + link: 'https://pixoeditor.com/privacy-policy/' + } + }, + // MIME types to accept + mimes : ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/x-ms-bmp'], + // HTML of this editor + html : '
    ', + // called on initialization of elFinder cmd edit (this: this editor's config object) + setup : function(opts, fm) { + if (fm.UA.ltIE8 || !opts.extraOptions || !opts.extraOptions.pixo || !opts.extraOptions.pixo.apikey) { + this.disabled = true; + } else { + this.editorOpts = opts.extraOptions.pixo; + } + }, + // Initialization of editing node (this: this editors HTML node) + init : function(id, file, content, fm) { + initImgTag.call(this, id, file, content, fm); + }, + // Get data uri scheme (this: this editors HTML node) + getContent : function() { + return $(this).children('img:first').attr('src'); + }, + // Launch Pixo editor when dialog open + load : function(base) { + var self = this, + fm = this.fm, + $base = $(base), + node = $base.children('img:first'), + dialog = $base.closest('.ui-dialog'), + elfNode = fm.getUI(), + dfrd = $.Deferred(), + container = $('#elfinder-pixo-container'), + init = function(onload) { + var opts; + + if (!container.length) { + container = $('
    ').css({ + position: 'fixed', + top: 0, + right: 0, + width: '100%', + height: $(window).height(), + overflow: 'hidden' + }).hide().appendTo(elfNode.hasClass('elfinder-fullscreen')? elfNode : 'body'); + // bind switch fullscreen event + elfNode.on('resize.'+fm.namespace, function(e, data) { + e.preventDefault(); + e.stopPropagation(); + data && data.fullscreen && container.appendTo(data.fullscreen === 'on'? elfNode : 'body'); + }); + fm.bind('destroy', function() { + editor && editor.cancelEditing(); + container.remove(); + }); + } else { + // always moves to last + container.appendTo(container.parent()); + } + node.on('click', launch); + // Constructor options + opts = Object.assign({ + type: 'child', + parent: container.get(0), + onSave: function(arg) { + // Check current file.hash, all callbacks are called on multiple instances + var mime = arg.toBlob().type, + ext = getExtention(mime, fm), + draw = function(url) { + node.one('load error', function() { + node.data('loading') && node.data('loading')(true); + }) + .attr('crossorigin', 'anonymous') + .attr('src', url); + }, + url = arg.toDataURL(); + node.data('loading')(); + delete base._canvas; + if (node.data('ext') !== ext) { + changeImageType(url, self.file.mime).done(function(res, cv) { + if (cv) { + base._canvas = canvas = cv; + quty.trigger('change'); + qBase && qBase.show(); + } + draw(res); + }).fail(function() { + dialog.trigger('changeType', { + extention: ext, + mime : mime + }); + draw(url); + }); + } else { + draw(url); + } + }, + onClose: function() { + dialog.removeClass(fm.res('class', 'preventback')); + fm.toggleMaximize(container, false); + container.hide(); + fm.toFront(dialog); + } + }, self.confObj.editorOpts); + // trigger event 'editEditorPrepare' + self.trigger('Prepare', { + node: base, + editorObj: Pixo, + instance: void(0), + opts: opts + }); + // make editor instance + editor = new Pixo.Bridge(opts); + dfrd.resolve(editor); + $base.on('saveAsFail', launch); + if (onload) { + onload(); + } + }, + launch = function() { + dialog.addClass(fm.res('class', 'preventback')); + fm.toggleMaximize(container, true); + fm.toFront(container); + container.show().data('curhash', self.file.hash); + editor.edit(node.get(0)); + node.data('loading')(true); + }, + qBase, quty, qutyTm, canvas, editor; + + node.data('loading')(); + + // jpeg quality controls + if (self.file.mime === 'image/jpeg') { + quty = $('') + .attr('min', '1') + .attr('max', '100') + .attr('title', '1 - 100') + .on('change', function() { + var q = quty.val(); + qutyTm && cancelAnimationFrame(qutyTm); + qutyTm = requestAnimationFrame(function() { + if (canvas) { + canvas.toBlob(function(blob) { + blob && quty.next('span').text(' (' + fm.formatSize(blob.size) + ')'); + }, 'image/jpeg', Math.max(Math.min(q, 100), 1) / 100); + } + }); + }) + .val(fm.storage('jpgQuality') || fm.option('jpgQuality')); + qBase = $('
    ') + .hide() + .append( + $('').html(fm.i18n('quality') + ' : '), quty, $('') + ) + .prependTo($base.parent().next()); + $base.data('quty', quty); + } + + // load script then init + if (typeof Pixo === 'undefined') { + fm.loadScript(['https://pixoeditor.com:8443/editor/scripts/bridge.m.js'], function() { + init(launch); + }, {loadType: 'tag'}); + } else { + init(); + launch(); + } + return dfrd; + }, + // Convert content url to data uri scheme to save content + save : function(base) { + var self = this, + $base = $(base), + node = $base.children('img:first'), + q; + if (base._canvas) { + q = $base.data('quty')? Math.max(Math.min($base.data('quty').val(), 100), 1) / 100 : void(0); + node.attr('src', base._canvas.toDataURL(self.file.mime, q)); + } else if (node.attr('src').substr(0, 5) !== 'data:') { + node.attr('src', imgBase64(node, this.file.mime)); + } + }, + close : function(base, editor) { + editor && editor.destroy(); + } + }, + { + // Adobe Creative SDK Creative Tools Image Editor UI + // MIME types to accept + info : { + id : 'creativecloud', + name : 'Creative Cloud', + iconImg : 'img/editor-icons.png 0 -192', + dataScheme: true, + schemeContent: true, + single: true, + canMakeEmpty: false, + integrate: { + title: 'Adobe Creative Cloud', + link: 'https://www.adobe.io/apis/creativecloud.html' + } + }, + mimes : ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/x-ms-bmp'], + // HTML of this editor + html : '
    ', + // called on initialization of elFinder cmd edit (this: this editor's config object) + setup : function(opts, fm) { + if (fm.UA.ltIE8 || !opts.extraOptions || !opts.extraOptions.creativeCloudApiKey) { + this.disabled = true; + } else { + this.apiKey = opts.extraOptions.creativeCloudApiKey; + } + }, + // Initialization of editing node (this: this editors HTML node) + init : function(id, file, content, fm) { + initImgTag.call(this, id, file, content, fm); + }, + // Get data uri scheme (this: this editors HTML node) + getContent : function() { + return $(this).children('img:first').attr('src'); + }, + // Launch Aviary Feather editor when dialog open + load : function(base) { + var self = this, + fm = this.fm, + node = $(base).children('img:first'), + dialog = $(base).closest('.ui-dialog'), + elfNode = fm.getUI(), + dfrd = $.Deferred(), + container = $('#elfinder-aviary-container'), + init = function(onload) { + var getLang = function() { + var langMap = { + 'zh_TW' : 'zh_HANT', + 'zh_CN' : 'zh_HANS' + }; + return langMap[fm.lang]? langMap[fm.lang] : fm.lang; + }, opts; + + if (!container.length) { + container = $('
    ').css({ + position: 'fixed', + top: 0, + right: 0, + width: '100%', + height: $(window).height(), + overflow: 'auto' + }).hide().appendTo(elfNode.hasClass('elfinder-fullscreen')? elfNode : 'body'); + // bind switch fullscreen event + elfNode.on('resize.'+fm.namespace, function(e, data) { + e.preventDefault(); + e.stopPropagation(); + data && data.fullscreen && container.appendTo(data.fullscreen === 'on'? elfNode : 'body'); + }); + fm.bind('destroy', function() { + container.remove(); + }); + } else { + // always moves to last + container.appendTo(container.parent()); + } + node.on('click', launch).data('loading')(); + opts = { + apiKey: self.confObj.apiKey, + onSave: function(imageID, newURL) { + var ext; + featherEditor.showWaitIndicator(); + ext = newURL.replace(/.+\.([^.]+)$/, '$1'); + if (node.data('ext') !== ext) { + node.closest('.ui-dialog').trigger('changeType', { + extention: ext, + mime : ext2mime[ext] + }); + } + node.on('load error', function() { + node.data('loading')(true); + }) + .attr('crossorigin', 'anonymous') + .attr('src', newURL) + .data('loading')(); + featherEditor.close(); + }, + onLoad: onload || function(){}, + onClose: function() { + dialog.removeClass(fm.res('class', 'preventback')); + fm.toggleMaximize(container, false); + $(container).hide(); + }, + appendTo: container.get(0), + maxSize: 2048, + language: getLang() + }; + // trigger event 'editEditorPrepare' + self.trigger('Prepare', { + node: base, + editorObj: Aviary, + instance: void(0), + opts: opts + }); + featherEditor = new Aviary.Feather(opts); + // return editor instance + dfrd.resolve(featherEditor); + $(base).on('saveAsFail', launch); + }, + launch = function() { + dialog.addClass(fm.res('class', 'preventback')); + fm.toggleMaximize(container, true); + fm.toFront(container); + $(container).show(); + featherEditor.launch({ + image: node.attr('id'), + url: node.attr('src') + }); + node.data('loading')(true); + }, + featherEditor, extraOpts; + + // load script then init + if (typeof Aviary === 'undefined') { + fm.loadScript(['https://dme0ih8comzn4.cloudfront.net/imaging/v3/editor.js'], function() { + init(launch); + }, {loadType: 'tag'}); + } else { + init(); + launch(); + } + return dfrd; + }, + // Convert content url to data uri scheme to save content + save : function(base) { + var node = $(base).children('img:first'); + if (node.attr('src').substr(0, 5) !== 'data:') { + node.attr('src', imgBase64(node, this.file.mime)); + } + } + }, + { + // ACE Editor + // called on initialization of elFinder cmd edit (this: this editor's config object) + setup : function(opts, fm) { + if (fm.UA.ltIE8 || !fm.options.cdns.ace) { + this.disabled = true; + } + }, + // `mimes` is not set for support everything kind of text file + info : { + id : 'aceeditor', + name : 'ACE Editor', + iconImg : 'img/editor-icons.png 0 -96' + }, + load : function(textarea) { + var self = this, + fm = this.fm, + dfrd = $.Deferred(), + cdn = fm.options.cdns.ace, + start = function() { + var editor, editorBase, mode, + ta = $(textarea), + taBase = ta.parent(), + dialog = taBase.parent(), + id = textarea.id + '_ace', + ext = self.file.name.replace(/^.+\.([^.]+)|(.+)$/, '$1$2').toLowerCase(), + // MIME/mode map + mimeMode = { + 'text/x-php' : 'php', + 'application/x-php' : 'php', + 'text/html' : 'html', + 'application/xhtml+xml' : 'html', + 'text/javascript' : 'javascript', + 'application/javascript' : 'javascript', + 'text/css' : 'css', + 'text/x-c' : 'c_cpp', + 'text/x-csrc' : 'c_cpp', + 'text/x-chdr' : 'c_cpp', + 'text/x-c++' : 'c_cpp', + 'text/x-c++src' : 'c_cpp', + 'text/x-c++hdr' : 'c_cpp', + 'text/x-shellscript' : 'sh', + 'application/x-csh' : 'sh', + 'text/x-python' : 'python', + 'text/x-java' : 'java', + 'text/x-java-source' : 'java', + 'text/x-ruby' : 'ruby', + 'text/x-perl' : 'perl', + 'application/x-perl' : 'perl', + 'text/x-sql' : 'sql', + 'text/xml' : 'xml', + 'application/docbook+xml' : 'xml', + 'application/xml' : 'xml' + }; + + // set base height + taBase.height(taBase.height()); + + // set basePath of ace + ace.config.set('basePath', cdn); + + // Base node of Ace editor + editorBase = $('
    ').text(ta.val()).insertBefore(ta.hide()); + + // Editor flag + ta.data('ace', true); + + // Aceeditor instance + editor = ace.edit(id); + + // Ace editor configure + editor.$blockScrolling = Infinity; + editor.setOptions({ + theme: 'ace/theme/monokai', + fontSize: '14px', + wrap: true, + }); + ace.config.loadModule('ace/ext/modelist', function() { + // detect mode + mode = ace.require('ace/ext/modelist').getModeForPath('/' + self.file.name).name; + if (mode === 'text') { + if (mimeMode[self.file.mime]) { + mode = mimeMode[self.file.mime]; + } + } + // show MIME:mode in title bar + taBase.prev().children('.elfinder-dialog-title').append(' (' + self.file.mime + ' : ' + mode.split(/[\/\\]/).pop() + ')'); + editor.setOptions({ + mode: 'ace/mode/' + mode + }); + if (dfrd.state() === 'resolved') { + dialog.trigger('resize'); + } + }); + ace.config.loadModule('ace/ext/language_tools', function() { + ace.require('ace/ext/language_tools'); + editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + enableLiveAutocompletion: false + }); + }); + ace.config.loadModule('ace/ext/settings_menu', function() { + ace.require('ace/ext/settings_menu').init(editor); + }); + + // Short cuts + editor.commands.addCommand({ + name : "saveFile", + bindKey: { + win : 'Ctrl-s', + mac : 'Command-s' + }, + exec: function(editor) { + self.doSave(); + } + }); + editor.commands.addCommand({ + name : "closeEditor", + bindKey: { + win : 'Ctrl-w|Ctrl-q', + mac : 'Command-w|Command-q' + }, + exec: function(editor) { + self.doCancel(); + } + }); + + editor.resize(); + + // TextArea button and Setting button + $('
    ').css('float', 'left') + .append( + $('') + .button({ + icons: { + primary: 'ui-icon-gear', + secondary: 'ui-icon-triangle-1-e' + }, + text: false + }) + .on('click', function(){ + editor.showSettingsMenu(); + $('#ace_settingsmenu') + .css('font-size', '80%') + .find('div[contains="setOptions"]').hide().end() + .parent().appendTo($('#elfinder')); + }) + ) + .prependTo(taBase.next()); + + // trigger event 'editEditorPrepare' + self.trigger('Prepare', { + node: textarea, + editorObj: ace, + instance: editor, + opts: {} + }); + + //dialog.trigger('resize'); + dfrd.resolve(editor); + }; + + // check ace & start + if (!self.confObj.loader) { + self.confObj.loader = $.Deferred(); + self.fm.loadScript([ cdn+'/ace.js' ], function() { + self.confObj.loader.resolve(); + }, void 0, {obj: window, name: 'ace'}); + } + self.confObj.loader.done(start); + + return dfrd; + }, + close : function(textarea, instance) { + instance && instance.destroy(); + }, + save : function(textarea, instance) { + instance && $(textarea).data('ace') && (textarea.value = instance.session.getValue()); + }, + focus : function(textarea, instance) { + instance && $(textarea).data('ace') && instance.focus(); + }, + resize : function(textarea, instance, e, data) { + instance && instance.resize(); + } + }, + { + // CodeMirror + // called on initialization of elFinder cmd edit (this: this editor's config object) + setup : function(opts, fm) { + if (fm.UA.ltIE10 || !fm.options.cdns.codemirror) { + this.disabled = true; + } + }, + // `mimes` is not set for support everything kind of text file + info : { + id : 'codemirror', + name : 'CodeMirror', + iconImg : 'img/editor-icons.png 0 -176' + }, + load : function(textarea) { + var fm = this.fm, + cmUrl = fm.options.cdns.codemirror, + dfrd = $.Deferred(), + self = this, + start = function(CodeMirror) { + var ta = $(textarea), + base = ta.parent(), + editor, editorBase, opts; + + // set base height + base.height(base.height()); + + // CodeMirror configure options + opts = { + lineNumbers: true, + lineWrapping: true, + extraKeys : { + 'Ctrl-S': function() { self.doSave(); }, + 'Ctrl-Q': function() { self.doCancel(); }, + 'Ctrl-W': function() { self.doCancel(); } + } + }; + + // trigger event 'editEditorPrepare' + self.trigger('Prepare', { + node: textarea, + editorObj: CodeMirror, + instance: void(0), + opts: opts + }); + + // CodeMirror configure + editor = CodeMirror.fromTextArea(textarea, opts); + + // return editor instance + dfrd.resolve(editor); + + // Auto mode set + var info, m, mode, spec; + if (! info) { + info = CodeMirror.findModeByMIME(self.file.mime); + } + if (! info && (m = self.file.name.match(/.+\.([^.]+)$/))) { + info = CodeMirror.findModeByExtension(m[1]); + } + if (info) { + CodeMirror.modeURL = useRequire? 'codemirror/mode/%N/%N.min' : cmUrl + '/mode/%N/%N.min.js'; + mode = info.mode; + spec = info.mime; + editor.setOption('mode', spec); + CodeMirror.autoLoadMode(editor, mode); + // show MIME:mode in title bar + base.prev().children('.elfinder-dialog-title').append(' (' + spec + ' : ' + mode + ')'); + } + + // editor base node + editorBase = $(editor.getWrapperElement()).css({ + // fix CSS conflict to SimpleMDE + padding: 0, + border: 'none' + }); + ta.data('cm', true); + + // fit height to base + editorBase.height('100%'); + + // TextArea button and Setting button + $('
    ').css('float', 'left') + .append( + $('").button({icons:{primary:"ui-icon-gear",secondary:"ui-icon-triangle-1-e"},text:!1}).on("click",function(){i.showSettingsMenu(),$("#ace_settingsmenu").css("font-size","80%").find('div[contains="setOptions"]').hide().end().parent().appendTo($("#elfinder"))})).prependTo(c.next()),t.trigger("Prepare",{node:e,editorObj:ace,instance:i,opts:{}}),n.resolve(i)};return t.confObj.loader||(t.confObj.loader=$.Deferred(),t.fm.loadScript([o+"/ace.js"],function(){t.confObj.loader.resolve()},void 0,{obj:window,name:"ace"})),t.confObj.loader.done(a),n},close:function(e,t){t&&t.destroy()},save:function(e,t){t&&$(e).data("ace")&&(e.value=t.session.getValue())},focus:function(e,t){t&&$(e).data("ace")&&t.focus()},resize:function(e,t,i,n){t&&t.resize()}},{setup:function(e,t){!t.UA.ltIE10&&t.options.cdns.codemirror||(this.disabled=!0)},info:{id:"codemirror",name:"CodeMirror",iconImg:"img/editor-icons.png 0 -176"},load:function(e){var t=this.fm,i=t.options.cdns.codemirror,o=$.Deferred(),a=this,r=function(t){var r,s,c,d=$(e),l=d.parent();l.height(l.height()),c={lineNumbers:!0,lineWrapping:!0,extraKeys:{"Ctrl-S":function(){a.doSave()},"Ctrl-Q":function(){a.doCancel()},"Ctrl-W":function(){a.doCancel()}}},a.trigger("Prepare",{node:e,editorObj:t,instance:void 0,opts:c}),r=t.fromTextArea(e,c),o.resolve(r);var p,m,f,u;p||(p=t.findModeByMIME(a.file.mime)),!p&&(m=a.file.name.match(/.+\.([^.]+)$/))&&(p=t.findModeByExtension(m[1])),p&&(t.modeURL=n?"codemirror/mode/%N/%N.min":i+"/mode/%N/%N.min.js",f=p.mode,u=p.mime,r.setOption("mode",u),t.autoLoadMode(r,f),l.prev().children(".elfinder-dialog-title").append(" ("+u+" : "+f+")")),s=$(r.getWrapperElement()).css({padding:0,border:"none"}),d.data("cm",!0),s.height("100%"),$('
    ').css("float","left").append($("").addClass(this._triggerClass). + html(!buttonImage ? buttonText : $("").attr( + { src:buttonImage, alt:buttonText, title:buttonText }))); + input[isRTL ? "before" : "after"](inst.trigger); + inst.trigger.click(function() { + if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) { + $.datepicker._hideDatepicker(); + } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) { + $.datepicker._hideDatepicker(); + $.datepicker._showDatepicker(input[0]); + } else { + $.datepicker._showDatepicker(input[0]); + } + return false; + }); + } + }, + + /* Apply the maximum length for the date format. */ + _autoSize: function(inst) { + if (this._get(inst, "autoSize") && !inst.inline) { + var findMax, max, maxI, i, + date = new Date(2009, 12 - 1, 20), // Ensure double digits + dateFormat = this._get(inst, "dateFormat"); + + if (dateFormat.match(/[DM]/)) { + findMax = function(names) { + max = 0; + maxI = 0; + for (i = 0; i < names.length; i++) { + if (names[i].length > max) { + max = names[i].length; + maxI = i; + } + } + return maxI; + }; + date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? + "monthNames" : "monthNamesShort")))); + date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? + "dayNames" : "dayNamesShort"))) + 20 - date.getDay()); + } + inst.input.attr("size", this._formatDate(inst, date).length); + } + }, + + /* Attach an inline date picker to a div. */ + _inlineDatepicker: function(target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) { + return; + } + divSpan.addClass(this.markerClassName).append(inst.dpDiv); + $.data(target, "datepicker", inst); + this._setDate(inst, this._getDefaultDate(inst), true); + this._updateDatepicker(inst); + this._updateAlternate(inst); + //If disabled option is true, disable the datepicker before showing it (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements + // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height + inst.dpDiv.css( "display", "block" ); + }, + + /* Pop-up the date picker in a "dialog" box. + * @param input element - ignored + * @param date string or Date - the initial date to display + * @param onSelect function - the function to call when a date is selected + * @param settings object - update the dialog date picker instance's settings (anonymous object) + * @param pos int[2] - coordinates for the dialog's position within the screen or + * event - with x/y coordinates or + * leave empty for default (screen centre) + * @return the manager object + */ + _dialogDatepicker: function(input, date, onSelect, settings, pos) { + var id, browserWidth, browserHeight, scrollX, scrollY, + inst = this._dialogInst; // internal instance + + if (!inst) { + this.uuid += 1; + id = "dp" + this.uuid; + this._dialogInput = $(""); + this._dialogInput.keydown(this._doKeyDown); + $("body").append(this._dialogInput); + inst = this._dialogInst = this._newInst(this._dialogInput, false); + inst.settings = {}; + $.data(this._dialogInput[0], "datepicker", inst); + } + datepicker_extendRemove(inst.settings, settings || {}); + date = (date && date.constructor === Date ? this._formatDate(inst, date) : date); + this._dialogInput.val(date); + + this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); + if (!this._pos) { + browserWidth = document.documentElement.clientWidth; + browserHeight = document.documentElement.clientHeight; + scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + scrollY = document.documentElement.scrollTop || document.body.scrollTop; + this._pos = // should use actual width/height below + [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; + } + + // move input on screen for focus, but hidden behind dialog + this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px"); + inst.settings.onSelect = onSelect; + this._inDialog = true; + this.dpDiv.addClass(this._dialogClass); + this._showDatepicker(this._dialogInput[0]); + if ($.blockUI) { + $.blockUI(this.dpDiv); + } + $.data(this._dialogInput[0], "datepicker", inst); + return this; + }, + + /* Detach a datepicker from its control. + * @param target element - the target input field or division or span + */ + _destroyDatepicker: function(target) { + var nodeName, + $target = $(target), + inst = $.data(target, "datepicker"); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + $.removeData(target, "datepicker"); + if (nodeName === "input") { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName). + unbind("focus", this._showDatepicker). + unbind("keydown", this._doKeyDown). + unbind("keypress", this._doKeyPress). + unbind("keyup", this._doKeyUp); + } else if (nodeName === "div" || nodeName === "span") { + $target.removeClass(this.markerClassName).empty(); + } + + if ( datepicker_instActive === inst ) { + datepicker_instActive = null; + } + }, + + /* Enable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _enableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, "datepicker"); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = false; + inst.trigger.filter("button"). + each(function() { this.disabled = false; }).end(). + filter("img").css({opacity: "1.0", cursor: ""}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().removeClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", false); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + }, + + /* Disable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _disableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, "datepicker"); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = true; + inst.trigger.filter("button"). + each(function() { this.disabled = true; }).end(). + filter("img").css({opacity: "0.5", cursor: "default"}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().addClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", true); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = target; + }, + + /* Is the first field in a jQuery collection disabled as a datepicker? + * @param target element - the target input field or division or span + * @return boolean - true if disabled, false if enabled + */ + _isDisabledDatepicker: function(target) { + if (!target) { + return false; + } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] === target) { + return true; + } + } + return false; + }, + + /* Retrieve the instance data for the target control. + * @param target element - the target input field or division or span + * @return object - the associated instance data + * @throws error if a jQuery problem getting data + */ + _getInst: function(target) { + try { + return $.data(target, "datepicker"); + } + catch (err) { + throw "Missing instance data for this datepicker"; + } + }, + + /* Update or retrieve the settings for a date picker attached to an input field or division. + * @param target element - the target input field or division or span + * @param name object - the new settings to update or + * string - the name of the setting to change or retrieve, + * when retrieving also "all" for all instance settings or + * "defaults" for all global defaults + * @param value any - the new value for the setting + * (omit if above is an object or to retrieve a value) + */ + _optionDatepicker: function(target, name, value) { + var settings, date, minDate, maxDate, + inst = this._getInst(target); + + if (arguments.length === 2 && typeof name === "string") { + return (name === "defaults" ? $.extend({}, $.datepicker._defaults) : + (inst ? (name === "all" ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + + settings = name || {}; + if (typeof name === "string") { + settings = {}; + settings[name] = value; + } + + if (inst) { + if (this._curInst === inst) { + this._hideDatepicker(); + } + + date = this._getDateDatepicker(target, true); + minDate = this._getMinMaxDate(inst, "min"); + maxDate = this._getMinMaxDate(inst, "max"); + datepicker_extendRemove(inst.settings, settings); + // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided + if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) { + inst.settings.minDate = this._formatDate(inst, minDate); + } + if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) { + inst.settings.maxDate = this._formatDate(inst, maxDate); + } + if ( "disabled" in settings ) { + if ( settings.disabled ) { + this._disableDatepicker(target); + } else { + this._enableDatepicker(target); + } + } + this._attachments($(target), inst); + this._autoSize(inst); + this._setDate(inst, date); + this._updateAlternate(inst); + this._updateDatepicker(inst); + } + }, + + // change method deprecated + _changeDatepicker: function(target, name, value) { + this._optionDatepicker(target, name, value); + }, + + /* Redraw the date picker attached to an input field or division. + * @param target element - the target input field or division or span + */ + _refreshDatepicker: function(target) { + var inst = this._getInst(target); + if (inst) { + this._updateDatepicker(inst); + } + }, + + /* Set the dates for a jQuery selection. + * @param target element - the target input field or division or span + * @param date Date - the new date + */ + _setDateDatepicker: function(target, date) { + var inst = this._getInst(target); + if (inst) { + this._setDate(inst, date); + this._updateDatepicker(inst); + this._updateAlternate(inst); + } + }, + + /* Get the date(s) for the first entry in a jQuery selection. + * @param target element - the target input field or division or span + * @param noDefault boolean - true if no default date is to be used + * @return Date - the current date + */ + _getDateDatepicker: function(target, noDefault) { + var inst = this._getInst(target); + if (inst && !inst.inline) { + this._setDateFromField(inst, noDefault); + } + return (inst ? this._getDate(inst) : null); + }, + + /* Handle keystrokes. */ + _doKeyDown: function(event) { + var onSelect, dateStr, sel, + inst = $.datepicker._getInst(event.target), + handled = true, + isRTL = inst.dpDiv.is(".ui-datepicker-rtl"); + + inst._keyEvent = true; + if ($.datepicker._datepickerShowing) { + switch (event.keyCode) { + case 9: $.datepicker._hideDatepicker(); + handled = false; + break; // hide on tab out + case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." + + $.datepicker._currentClass + ")", inst.dpDiv); + if (sel[0]) { + $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); + } + + onSelect = $.datepicker._get(inst, "onSelect"); + if (onSelect) { + dateStr = $.datepicker._formatDate(inst); + + // trigger custom callback + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); + } else { + $.datepicker._hideDatepicker(); + } + + return false; // don't submit the form + case 27: $.datepicker._hideDatepicker(); + break; // hide on escape + case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + break; // previous month/year on page up/+ ctrl + case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + break; // next month/year on page down/+ ctrl + case 35: if (event.ctrlKey || event.metaKey) { + $.datepicker._clearDate(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // clear on ctrl or command +end + case 36: if (event.ctrlKey || event.metaKey) { + $.datepicker._gotoToday(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // current on ctrl or command +home + case 37: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // -1 day on ctrl or command +left + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +left on Mac + break; + case 38: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, -7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // -1 week on ctrl or command +up + case 39: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // +1 day on ctrl or command +right + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +right + break; + case 40: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, +7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // +1 week on ctrl or command +down + default: handled = false; + } + } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home + $.datepicker._showDatepicker(this); + } else { + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Filter entered characters - based on date format. */ + _doKeyPress: function(event) { + var chars, chr, + inst = $.datepicker._getInst(event.target); + + if ($.datepicker._get(inst, "constrainInput")) { + chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat")); + chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode); + return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1); + } + }, + + /* Synchronise manual entry and field/alternate field. */ + _doKeyUp: function(event) { + var date, + inst = $.datepicker._getInst(event.target); + + if (inst.input.val() !== inst.lastVal) { + try { + date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + (inst.input ? inst.input.val() : null), + $.datepicker._getFormatConfig(inst)); + + if (date) { // only if valid + $.datepicker._setDateFromField(inst); + $.datepicker._updateAlternate(inst); + $.datepicker._updateDatepicker(inst); + } + } + catch (err) { + } + } + return true; + }, + + /* Pop-up the date picker for a given input field. + * If false returned from beforeShow event handler do not show. + * @param input element - the input field attached to the date picker or + * event - if triggered by focus + */ + _showDatepicker: function(input) { + input = input.target || input; + if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger + input = $("input", input.parentNode)[0]; + } + + if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here + return; + } + + var inst, beforeShow, beforeShowSettings, isFixed, + offset, showAnim, duration; + + inst = $.datepicker._getInst(input); + if ($.datepicker._curInst && $.datepicker._curInst !== inst) { + $.datepicker._curInst.dpDiv.stop(true, true); + if ( inst && $.datepicker._datepickerShowing ) { + $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] ); + } + } + + beforeShow = $.datepicker._get(inst, "beforeShow"); + beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; + if(beforeShowSettings === false){ + return; + } + datepicker_extendRemove(inst.settings, beforeShowSettings); + + inst.lastVal = null; + $.datepicker._lastInput = input; + $.datepicker._setDateFromField(inst); + + if ($.datepicker._inDialog) { // hide cursor + input.value = ""; + } + if (!$.datepicker._pos) { // position below input + $.datepicker._pos = $.datepicker._findPos(input); + $.datepicker._pos[1] += input.offsetHeight; // add the height + } + + isFixed = false; + $(input).parents().each(function() { + isFixed |= $(this).css("position") === "fixed"; + return !isFixed; + }); + + offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; + $.datepicker._pos = null; + //to avoid flashes on Firefox + inst.dpDiv.empty(); + // determine sizing offscreen + inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); + $.datepicker._updateDatepicker(inst); + // fix width for dynamic number of date pickers + // and adjust position before showing + offset = $.datepicker._checkOffset(inst, offset, isFixed); + inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ? + "static" : (isFixed ? "fixed" : "absolute")), display: "none", + left: offset.left + "px", top: offset.top + "px"}); + + if (!inst.inline) { + showAnim = $.datepicker._get(inst, "showAnim"); + duration = $.datepicker._get(inst, "duration"); + inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 ); + $.datepicker._datepickerShowing = true; + + if ( $.effects && $.effects.effect[ showAnim ] ) { + inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration); + } else { + inst.dpDiv[showAnim || "show"](showAnim ? duration : null); + } + + if ( $.datepicker._shouldFocusInput( inst ) ) { + inst.input.focus(); + } + + $.datepicker._curInst = inst; + } + }, + + /* Generate the date picker content. */ + _updateDatepicker: function(inst) { + this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) + datepicker_instActive = inst; // for delegate hover events + inst.dpDiv.empty().append(this._generateHTML(inst)); + this._attachHandlers(inst); + + var origyearshtml, + numMonths = this._getNumberOfMonths(inst), + cols = numMonths[1], + width = 17, + activeCell = inst.dpDiv.find( "." + this._dayOverClass + " a" ); + + if ( activeCell.length > 0 ) { + datepicker_handleMouseover.apply( activeCell.get( 0 ) ); + } + + inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); + if (cols > 1) { + inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em"); + } + inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") + + "Class"]("ui-datepicker-multi"); + inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") + + "Class"]("ui-datepicker-rtl"); + + if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) { + inst.input.focus(); + } + + // deffered render of the years select (to avoid flashes on Firefox) + if( inst.yearshtml ){ + origyearshtml = inst.yearshtml; + setTimeout(function(){ + //assure that inst.yearshtml didn't change. + if( origyearshtml === inst.yearshtml && inst.yearshtml ){ + inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml); + } + origyearshtml = inst.yearshtml = null; + }, 0); + } + }, + + // #6694 - don't focus the input if it's already focused + // this breaks the change event in IE + // Support: IE and jQuery <1.9 + _shouldFocusInput: function( inst ) { + return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" ); + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function(inst, offset, isFixed) { + var dpWidth = inst.dpDiv.outerWidth(), + dpHeight = inst.dpDiv.outerHeight(), + inputWidth = inst.input ? inst.input.outerWidth() : 0, + inputHeight = inst.input ? inst.input.outerHeight() : 0, + viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()), + viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop()); + + offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? + Math.abs(offset.left + dpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? + Math.abs(dpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function(obj) { + var position, + inst = this._getInst(obj), + isRTL = this._get(inst, "isRTL"); + + while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) { + obj = obj[isRTL ? "previousSibling" : "nextSibling"]; + } + + position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Hide the date picker from view. + * @param input element - the input field attached to the date picker + */ + _hideDatepicker: function(input) { + var showAnim, duration, postProcess, onClose, + inst = this._curInst; + + if (!inst || (input && inst !== $.data(input, "datepicker"))) { + return; + } + + if (this._datepickerShowing) { + showAnim = this._get(inst, "showAnim"); + duration = this._get(inst, "duration"); + postProcess = function() { + $.datepicker._tidyDialog(inst); + }; + + // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed + if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { + inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess); + } else { + inst.dpDiv[(showAnim === "slideDown" ? "slideUp" : + (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess); + } + + if (!showAnim) { + postProcess(); + } + this._datepickerShowing = false; + + onClose = this._get(inst, "onClose"); + if (onClose) { + onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]); + } + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" }); + if ($.blockUI) { + $.unblockUI(); + $("body").append(this.dpDiv); + } + } + this._inDialog = false; + } + }, + + /* Tidy up after a dialog display. */ + _tidyDialog: function(inst) { + inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar"); + }, + + /* Close date picker if clicked elsewhere. */ + _checkExternalClick: function(event) { + if (!$.datepicker._curInst) { + return; + } + + var $target = $(event.target), + inst = $.datepicker._getInst($target[0]); + + if ( ( ( $target[0].id !== $.datepicker._mainDivId && + $target.parents("#" + $.datepicker._mainDivId).length === 0 && + !$target.hasClass($.datepicker.markerClassName) && + !$target.closest("." + $.datepicker._triggerClass).length && + $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) || + ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) { + $.datepicker._hideDatepicker(); + } + }, + + /* Adjust one of the date sub-fields. */ + _adjustDate: function(id, offset, period) { + var target = $(id), + inst = this._getInst(target[0]); + + if (this._isDisabledDatepicker(target[0])) { + return; + } + this._adjustInstDate(inst, offset + + (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning + period); + this._updateDatepicker(inst); + }, + + /* Action for current link. */ + _gotoToday: function(id) { + var date, + target = $(id), + inst = this._getInst(target[0]); + + if (this._get(inst, "gotoCurrent") && inst.currentDay) { + inst.selectedDay = inst.currentDay; + inst.drawMonth = inst.selectedMonth = inst.currentMonth; + inst.drawYear = inst.selectedYear = inst.currentYear; + } else { + date = new Date(); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + } + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a new month/year. */ + _selectMonthYear: function(id, select, period) { + var target = $(id), + inst = this._getInst(target[0]); + + inst["selected" + (period === "M" ? "Month" : "Year")] = + inst["draw" + (period === "M" ? "Month" : "Year")] = + parseInt(select.options[select.selectedIndex].value,10); + + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a day. */ + _selectDay: function(id, month, year, td) { + var inst, + target = $(id); + + if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { + return; + } + + inst = this._getInst(target[0]); + inst.selectedDay = inst.currentDay = $("a", td).html(); + inst.selectedMonth = inst.currentMonth = month; + inst.selectedYear = inst.currentYear = year; + this._selectDate(id, this._formatDate(inst, + inst.currentDay, inst.currentMonth, inst.currentYear)); + }, + + /* Erase the input field and hide the date picker. */ + _clearDate: function(id) { + var target = $(id); + this._selectDate(target, ""); + }, + + /* Update the input field with the selected date. */ + _selectDate: function(id, dateStr) { + var onSelect, + target = $(id), + inst = this._getInst(target[0]); + + dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); + if (inst.input) { + inst.input.val(dateStr); + } + this._updateAlternate(inst); + + onSelect = this._get(inst, "onSelect"); + if (onSelect) { + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback + } else if (inst.input) { + inst.input.trigger("change"); // fire the change event + } + + if (inst.inline){ + this._updateDatepicker(inst); + } else { + this._hideDatepicker(); + this._lastInput = inst.input[0]; + if (typeof(inst.input[0]) !== "object") { + inst.input.focus(); // restore focus + } + this._lastInput = null; + } + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst) { + var altFormat, date, dateStr, + altField = this._get(inst, "altField"); + + if (altField) { // update alternate field too + altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat"); + date = this._getDate(inst); + dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); + $(altField).each(function() { $(this).val(dateStr); }); + } + }, + + /* Set as beforeShowDay function to prevent selection of weekends. + * @param date Date - the date to customise + * @return [boolean, string] - is this date selectable?, what is its CSS class? + */ + noWeekends: function(date) { + var day = date.getDay(); + return [(day > 0 && day < 6), ""]; + }, + + /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * @param date Date - the date to get the week for + * @return number - the number of the week within the year that contains this date + */ + iso8601Week: function(date) { + var time, + checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + }, + + /* Parse a string value into a date object. + * See formatDate below for the possible formats. + * + * @param format string - the expected format of the date + * @param value string - the date in the above format + * @param settings Object - attributes include: + * shortYearCutoff number - the cutoff year for determining the century (optional) + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return Date - the extracted date value or null if value is blank + */ + parseDate: function (format, value, settings) { + if (format == null || value == null) { + throw "Invalid arguments"; + } + + value = (typeof value === "object" ? value.toString() : value + ""); + if (value === "") { + return null; + } + + var iFormat, dim, extra, + iValue = 0, + shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff, + shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : + new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)), + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + year = -1, + month = -1, + day = -1, + doy = -1, + literal = false, + date, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Extract a number from the string value + getNumber = function(match) { + var isDoubled = lookAhead(match), + size = (match === "@" ? 14 : (match === "!" ? 20 : + (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))), + minSize = (match === "y" ? size : 1), + digits = new RegExp("^\\d{" + minSize + "," + size + "}"), + num = value.substring(iValue).match(digits); + if (!num) { + throw "Missing number at position " + iValue; + } + iValue += num[0].length; + return parseInt(num[0], 10); + }, + // Extract a name from the string value and convert to an index + getName = function(match, shortNames, longNames) { + var index = -1, + names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { + return [ [k, v] ]; + }).sort(function (a, b) { + return -(a[1].length - b[1].length); + }); + + $.each(names, function (i, pair) { + var name = pair[1]; + if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) { + index = pair[0]; + iValue += name.length; + return false; + } + }); + if (index !== -1) { + return index + 1; + } else { + throw "Unknown name at position " + iValue; + } + }, + // Confirm that a literal character matches the string value + checkLiteral = function() { + if (value.charAt(iValue) !== format.charAt(iFormat)) { + throw "Unexpected literal at position " + iValue; + } + iValue++; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + checkLiteral(); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + day = getNumber("d"); + break; + case "D": + getName("D", dayNamesShort, dayNames); + break; + case "o": + doy = getNumber("o"); + break; + case "m": + month = getNumber("m"); + break; + case "M": + month = getName("M", monthNamesShort, monthNames); + break; + case "y": + year = getNumber("y"); + break; + case "@": + date = new Date(getNumber("@")); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "!": + date = new Date((getNumber("!") - this._ticksTo1970) / 10000); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "'": + if (lookAhead("'")){ + checkLiteral(); + } else { + literal = true; + } + break; + default: + checkLiteral(); + } + } + } + + if (iValue < value.length){ + extra = value.substr(iValue); + if (!/^\s+/.test(extra)) { + throw "Extra/unparsed characters found in date: " + extra; + } + } + + if (year === -1) { + year = new Date().getFullYear(); + } else if (year < 100) { + year += new Date().getFullYear() - new Date().getFullYear() % 100 + + (year <= shortYearCutoff ? 0 : -100); + } + + if (doy > -1) { + month = 1; + day = doy; + do { + dim = this._getDaysInMonth(year, month - 1); + if (day <= dim) { + break; + } + month++; + day -= dim; + } while (true); + } + + date = this._daylightSavingAdjust(new Date(year, month - 1, day)); + if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { + throw "Invalid date"; // E.g. 31/02/00 + } + return date; + }, + + /* Standard date formats. */ + ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) + COOKIE: "D, dd M yy", + ISO_8601: "yy-mm-dd", + RFC_822: "D, d M y", + RFC_850: "DD, dd-M-y", + RFC_1036: "D, d M y", + RFC_1123: "D, d M yy", + RFC_2822: "D, d M yy", + RSS: "D, d M y", // RFC 822 + TICKS: "!", + TIMESTAMP: "@", + W3C: "yy-mm-dd", // ISO 8601 + + _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), + + /* Format a date object into a string value. + * The format can be combinations of the following: + * d - day of month (no leading zero) + * dd - day of month (two digit) + * o - day of year (no leading zeros) + * oo - day of year (three digit) + * D - day name short + * DD - day name long + * m - month of year (no leading zero) + * mm - month of year (two digit) + * M - month name short + * MM - month name long + * y - year (two digit) + * yy - year (four digit) + * @ - Unix timestamp (ms since 01/01/1970) + * ! - Windows ticks (100ns since 01/01/0001) + * "..." - literal text + * '' - single quote + * + * @param format string - the desired format of the date + * @param date Date - the date value to format + * @param settings Object - attributes include: + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return string - the date in the above format + */ + formatDate: function (format, date, settings) { + if (!date) { + return ""; + } + + var iFormat, + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Format a number, with leading zero if necessary + formatNumber = function(match, value, len) { + var num = "" + value; + if (lookAhead(match)) { + while (num.length < len) { + num = "0" + num; + } + } + return num; + }, + // Format a name, short or long as requested + formatName = function(match, value, shortNames, longNames) { + return (lookAhead(match) ? longNames[value] : shortNames[value]); + }, + output = "", + literal = false; + + if (date) { + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + output += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + output += formatNumber("d", date.getDate(), 2); + break; + case "D": + output += formatName("D", date.getDay(), dayNamesShort, dayNames); + break; + case "o": + output += formatNumber("o", + Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); + break; + case "m": + output += formatNumber("m", date.getMonth() + 1, 2); + break; + case "M": + output += formatName("M", date.getMonth(), monthNamesShort, monthNames); + break; + case "y": + output += (lookAhead("y") ? date.getFullYear() : + (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100); + break; + case "@": + output += date.getTime(); + break; + case "!": + output += date.getTime() * 10000 + this._ticksTo1970; + break; + case "'": + if (lookAhead("'")) { + output += "'"; + } else { + literal = true; + } + break; + default: + output += format.charAt(iFormat); + } + } + } + } + return output; + }, + + /* Extract all possible characters from the date format. */ + _possibleChars: function (format) { + var iFormat, + chars = "", + literal = false, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + chars += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": case "m": case "y": case "@": + chars += "0123456789"; + break; + case "D": case "M": + return null; // Accept anything + case "'": + if (lookAhead("'")) { + chars += "'"; + } else { + literal = true; + } + break; + default: + chars += format.charAt(iFormat); + } + } + } + return chars; + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function(inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing date and initialise date picker. */ + _setDateFromField: function(inst, noDefault) { + if (inst.input.val() === inst.lastVal) { + return; + } + + var dateFormat = this._get(inst, "dateFormat"), + dates = inst.lastVal = inst.input ? inst.input.val() : null, + defaultDate = this._getDefaultDate(inst), + date = defaultDate, + settings = this._getFormatConfig(inst); + + try { + date = this.parseDate(dateFormat, dates, settings) || defaultDate; + } catch (event) { + dates = (noDefault ? "" : dates); + } + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + inst.currentDay = (dates ? date.getDate() : 0); + inst.currentMonth = (dates ? date.getMonth() : 0); + inst.currentYear = (dates ? date.getFullYear() : 0); + this._adjustInstDate(inst); + }, + + /* Retrieve the default date shown on opening. */ + _getDefaultDate: function(inst) { + return this._restrictMinMax(inst, + this._determineDate(inst, this._get(inst, "defaultDate"), new Date())); + }, + + /* A date may be specified as an exact value or a relative one. */ + _determineDate: function(inst, date, defaultDate) { + var offsetNumeric = function(offset) { + var date = new Date(); + date.setDate(date.getDate() + offset); + return date; + }, + offsetString = function(offset) { + try { + return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + offset, $.datepicker._getFormatConfig(inst)); + } + catch (e) { + // Ignore + } + + var date = (offset.toLowerCase().match(/^c/) ? + $.datepicker._getDate(inst) : null) || new Date(), + year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, + matches = pattern.exec(offset); + + while (matches) { + switch (matches[2] || "d") { + case "d" : case "D" : + day += parseInt(matches[1],10); break; + case "w" : case "W" : + day += parseInt(matches[1],10) * 7; break; + case "m" : case "M" : + month += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + case "y": case "Y" : + year += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + } + matches = pattern.exec(offset); + } + return new Date(year, month, day); + }, + newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) : + (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); + + newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate); + if (newDate) { + newDate.setHours(0); + newDate.setMinutes(0); + newDate.setSeconds(0); + newDate.setMilliseconds(0); + } + return this._daylightSavingAdjust(newDate); + }, + + /* Handle switch to/from daylight saving. + * Hours may be non-zero on daylight saving cut-over: + * > 12 when midnight changeover, but then cannot generate + * midnight datetime, so jump to 1AM, otherwise reset. + * @param date (Date) the date to check + * @return (Date) the corrected date + */ + _daylightSavingAdjust: function(date) { + if (!date) { + return null; + } + date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); + return date; + }, + + /* Set the date(s) directly. */ + _setDate: function(inst, date, noChange) { + var clear = !date, + origMonth = inst.selectedMonth, + origYear = inst.selectedYear, + newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); + + inst.selectedDay = inst.currentDay = newDate.getDate(); + inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); + inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); + if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) { + this._notifyChange(inst); + } + this._adjustInstDate(inst); + if (inst.input) { + inst.input.val(clear ? "" : this._formatDate(inst)); + } + }, + + /* Retrieve the date(s) directly. */ + _getDate: function(inst) { + var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null : + this._daylightSavingAdjust(new Date( + inst.currentYear, inst.currentMonth, inst.currentDay))); + return startDate; + }, + + /* Attach the onxxx handlers. These are declared statically so + * they work with static code transformers like Caja. + */ + _attachHandlers: function(inst) { + var stepMonths = this._get(inst, "stepMonths"), + id = "#" + inst.id.replace( /\\\\/g, "\\" ); + inst.dpDiv.find("[data-handler]").map(function () { + var handler = { + prev: function () { + $.datepicker._adjustDate(id, -stepMonths, "M"); + }, + next: function () { + $.datepicker._adjustDate(id, +stepMonths, "M"); + }, + hide: function () { + $.datepicker._hideDatepicker(); + }, + today: function () { + $.datepicker._gotoToday(id); + }, + selectDay: function () { + $.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this); + return false; + }, + selectMonth: function () { + $.datepicker._selectMonthYear(id, this, "M"); + return false; + }, + selectYear: function () { + $.datepicker._selectMonthYear(id, this, "Y"); + return false; + } + }; + $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]); + }); + }, + + /* Generate the HTML for the current state of the date picker. */ + _generateHTML: function(inst) { + var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, + controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, + monthNames, monthNamesShort, beforeShowDay, showOtherMonths, + selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, + cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, + printDate, dRow, tbody, daySettings, otherMonth, unselectable, + tempDate = new Date(), + today = this._daylightSavingAdjust( + new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time + isRTL = this._get(inst, "isRTL"), + showButtonPanel = this._get(inst, "showButtonPanel"), + hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"), + navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"), + numMonths = this._getNumberOfMonths(inst), + showCurrentAtPos = this._get(inst, "showCurrentAtPos"), + stepMonths = this._get(inst, "stepMonths"), + isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1), + currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : + new Date(inst.currentYear, inst.currentMonth, inst.currentDay))), + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + drawMonth = inst.drawMonth - showCurrentAtPos, + drawYear = inst.drawYear; + + if (drawMonth < 0) { + drawMonth += 12; + drawYear--; + } + if (maxDate) { + maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), + maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); + maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); + while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { + drawMonth--; + if (drawMonth < 0) { + drawMonth = 11; + drawYear--; + } + } + } + inst.drawMonth = drawMonth; + inst.drawYear = drawYear; + + prevText = this._get(inst, "prevText"); + prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), + this._getFormatConfig(inst))); + + prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? + "" + prevText + "" : + (hideIfNoPrevNext ? "" : "" + prevText + "")); + + nextText = this._get(inst, "nextText"); + nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), + this._getFormatConfig(inst))); + + next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? + "" + nextText + "" : + (hideIfNoPrevNext ? "" : "" + nextText + "")); + + currentText = this._get(inst, "currentText"); + gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today); + currentText = (!navigationAsDateFormat ? currentText : + this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); + + controls = (!inst.inline ? "" : ""); + + buttonPanel = (showButtonPanel) ? "
    " + (isRTL ? controls : "") + + (this._isInRange(inst, gotoDate) ? "" : "") + (isRTL ? "" : controls) + "
    " : ""; + + firstDay = parseInt(this._get(inst, "firstDay"),10); + firstDay = (isNaN(firstDay) ? 0 : firstDay); + + showWeek = this._get(inst, "showWeek"); + dayNames = this._get(inst, "dayNames"); + dayNamesMin = this._get(inst, "dayNamesMin"); + monthNames = this._get(inst, "monthNames"); + monthNamesShort = this._get(inst, "monthNamesShort"); + beforeShowDay = this._get(inst, "beforeShowDay"); + showOtherMonths = this._get(inst, "showOtherMonths"); + selectOtherMonths = this._get(inst, "selectOtherMonths"); + defaultDate = this._getDefaultDate(inst); + html = ""; + dow; + for (row = 0; row < numMonths[0]; row++) { + group = ""; + this.maxRows = 4; + for (col = 0; col < numMonths[1]; col++) { + selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); + cornerClass = " ui-corner-all"; + calender = ""; + if (isMultiMonth) { + calender += "
    "; + } + calender += "
    " + + (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") + + (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") + + this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, + row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers + "
    " + + ""; + thead = (showWeek ? "" : ""); + for (dow = 0; dow < 7; dow++) { // days of the week + day = (dow + firstDay) % 7; + thead += ""; + } + calender += thead + ""; + daysInMonth = this._getDaysInMonth(drawYear, drawMonth); + if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) { + inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); + } + leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; + curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate + numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) + this.maxRows = numRows; + printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); + for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows + calender += ""; + tbody = (!showWeek ? "" : ""); + for (dow = 0; dow < 7; dow++) { // create date picker days + daySettings = (beforeShowDay ? + beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]); + otherMonth = (printDate.getMonth() !== drawMonth); + unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || + (minDate && printDate < minDate) || (maxDate && printDate > maxDate); + tbody += ""; // display selectable date + printDate.setDate(printDate.getDate() + 1); + printDate = this._daylightSavingAdjust(printDate); + } + calender += tbody + ""; + } + drawMonth++; + if (drawMonth > 11) { + drawMonth = 0; + drawYear++; + } + calender += "
    " + this._get(inst, "weekHeader") + "= 5 ? " class='ui-datepicker-week-end'" : "") + ">" + + "" + dayNamesMin[day] + "
    " + + this._get(inst, "calculateWeek")(printDate) + "" + // actions + (otherMonth && !showOtherMonths ? " " : // display for other months + (unselectable ? "" + printDate.getDate() + "" : "" + printDate.getDate() + "")) + "
    " + (isMultiMonth ? "
    " + + ((numMonths[0] > 0 && col === numMonths[1]-1) ? "
    " : "") : ""); + group += calender; + } + html += group; + } + html += buttonPanel; + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + + var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, + changeMonth = this._get(inst, "changeMonth"), + changeYear = this._get(inst, "changeYear"), + showMonthAfterYear = this._get(inst, "showMonthAfterYear"), + html = "
    ", + monthHtml = ""; + + // month selection + if (secondary || !changeMonth) { + monthHtml += "" + monthNames[drawMonth] + ""; + } else { + inMinYear = (minDate && minDate.getFullYear() === drawYear); + inMaxYear = (maxDate && maxDate.getFullYear() === drawYear); + monthHtml += ""; + } + + if (!showMonthAfterYear) { + html += monthHtml + (secondary || !(changeMonth && changeYear) ? " " : ""); + } + + // year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ""; + if (secondary || !changeYear) { + html += "" + drawYear + ""; + } else { + // determine range of years to display + years = this._get(inst, "yearRange").split(":"); + thisYear = new Date().getFullYear(); + determineYear = function(value) { + var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + year = determineYear(years[0]); + endYear = Math.max(year, determineYear(years[1] || "")); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += ""; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + + html += this._get(inst, "yearSuffix"); + if (showMonthAfterYear) { + html += (secondary || !(changeMonth && changeYear) ? " " : "") + monthHtml; + } + html += "
    "; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function(inst, offset, period) { + var year = inst.drawYear + (period === "Y" ? offset : 0), + month = inst.drawMonth + (period === "M" ? offset : 0), + day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0), + date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day))); + + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period === "M" || period === "Y") { + this._notifyChange(inst); + } + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function(inst, date) { + var minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + newDate = (minDate && date < minDate ? minDate : date); + return (maxDate && newDate > maxDate ? maxDate : newDate); + }, + + /* Notify change of month/year. */ + _notifyChange: function(inst) { + var onChange = this._get(inst, "onChangeMonthYear"); + if (onChange) { + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + } + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function(inst) { + var numMonths = this._get(inst, "numberOfMonths"); + return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function(inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + "Date"), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function(year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function(year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function(inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst), + date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + + if (offset < 0) { + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + } + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function(inst, date) { + var yearSplit, currentYear, + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + minYear = null, + maxYear = null, + years = this._get(inst, "yearRange"); + if (years){ + yearSplit = years.split(":"); + currentYear = new Date().getFullYear(); + minYear = parseInt(yearSplit[0], 10); + maxYear = parseInt(yearSplit[1], 10); + if ( yearSplit[0].match(/[+\-].*/) ) { + minYear += currentYear; + } + if ( yearSplit[1].match(/[+\-].*/) ) { + maxYear += currentYear; + } + } + + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime()) && + (!minYear || date.getFullYear() >= minYear) && + (!maxYear || date.getFullYear() <= maxYear)); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function(inst) { + var shortYearCutoff = this._get(inst, "shortYearCutoff"); + shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return {shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"), + monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")}; + }, + + /* Format the given date for display. */ + _formatDate: function(inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day === "object" ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst)); + } +}); + +/* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global datepicker_instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function datepicker_bindHover(dpDiv) { + var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; + return dpDiv.delegate(selector, "mouseout", function() { + $(this).removeClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).removeClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).removeClass("ui-datepicker-next-hover"); + } + }) + .delegate( selector, "mouseover", datepicker_handleMouseover ); +} + +function datepicker_handleMouseover() { + if (!$.datepicker._isDisabledDatepicker( datepicker_instActive.inline? datepicker_instActive.dpDiv.parent()[0] : datepicker_instActive.input[0])) { + $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); + $(this).addClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).addClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).addClass("ui-datepicker-next-hover"); + } + } +} + +/* jQuery extend now ignores nulls! */ +function datepicker_extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = props[name]; + } + } + return target; +} + +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function(options){ + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick); + $.datepicker.initialized = true; + } + + /* Append datepicker main container to body if not exist. */ + if ($("#"+$.datepicker._mainDivId).length === 0) { + $("body").append($.datepicker.dpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + return this.each(function() { + typeof options === "string" ? + $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); +}; + +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.11.4"; + +return $.datepicker; + +})); diff --git a/include/thirdparty/jquery_ui/dialog.js b/include/thirdparty/jquery_ui/dialog.js new file mode 100644 index 0000000..2f978be --- /dev/null +++ b/include/thirdparty/jquery_ui/dialog.js @@ -0,0 +1,876 @@ +/*! + * jQuery UI Dialog 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/dialog/ + */ +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ + "jquery", + "./core", + "./widget", + "./button", + "./draggable", + "./mouse", + "./position", + "./resizable" + ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + +return $.widget( "ui.dialog", { + version: "1.11.4", + options: { + appendTo: "body", + autoOpen: true, + buttons: [], + closeOnEscape: true, + closeText: "Close", + dialogClass: "", + draggable: true, + hide: null, + height: "auto", + maxHeight: null, + maxWidth: null, + minHeight: 150, + minWidth: 150, + modal: false, + position: { + my: "center", + at: "center", + of: window, + collision: "fit", + // Ensure the titlebar is always visible + using: function( pos ) { + var topOffset = $( this ).css( pos ).offset().top; + if ( topOffset < 0 ) { + $( this ).css( "top", pos.top - topOffset ); + } + } + }, + resizable: true, + show: null, + title: null, + width: 300, + + // callbacks + beforeClose: null, + close: null, + drag: null, + dragStart: null, + dragStop: null, + focus: null, + open: null, + resize: null, + resizeStart: null, + resizeStop: null + }, + + sizeRelatedOptions: { + buttons: true, + height: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + width: true + }, + + resizableRelatedOptions: { + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true + }, + + _create: function() { + this.originalCss = { + display: this.element[ 0 ].style.display, + width: this.element[ 0 ].style.width, + minHeight: this.element[ 0 ].style.minHeight, + maxHeight: this.element[ 0 ].style.maxHeight, + height: this.element[ 0 ].style.height + }; + this.originalPosition = { + parent: this.element.parent(), + index: this.element.parent().children().index( this.element ) + }; + this.originalTitle = this.element.attr( "title" ); + this.options.title = this.options.title || this.originalTitle; + + this._createWrapper(); + + this.element + .show() + .removeAttr( "title" ) + .addClass( "ui-dialog-content ui-widget-content" ) + .appendTo( this.uiDialog ); + + this._createTitlebar(); + this._createButtonPane(); + + if ( this.options.draggable && $.fn.draggable ) { + this._makeDraggable(); + } + if ( this.options.resizable && $.fn.resizable ) { + this._makeResizable(); + } + + this._isOpen = false; + + this._trackFocus(); + }, + + _init: function() { + if ( this.options.autoOpen ) { + this.open(); + } + }, + + _appendTo: function() { + var element = this.options.appendTo; + if ( element && (element.jquery || element.nodeType) ) { + return $( element ); + } + return this.document.find( element || "body" ).eq( 0 ); + }, + + _destroy: function() { + var next, + originalPosition = this.originalPosition; + + this._untrackInstance(); + this._destroyOverlay(); + + this.element + .removeUniqueId() + .removeClass( "ui-dialog-content ui-widget-content" ) + .css( this.originalCss ) + // Without detaching first, the following becomes really slow + .detach(); + + this.uiDialog.stop( true, true ).remove(); + + if ( this.originalTitle ) { + this.element.attr( "title", this.originalTitle ); + } + + next = originalPosition.parent.children().eq( originalPosition.index ); + // Don't try to place the dialog next to itself (#8613) + if ( next.length && next[ 0 ] !== this.element[ 0 ] ) { + next.before( this.element ); + } else { + originalPosition.parent.append( this.element ); + } + }, + + widget: function() { + return this.uiDialog; + }, + + disable: $.noop, + enable: $.noop, + + close: function( event ) { + var activeElement, + that = this; + + if ( !this._isOpen || this._trigger( "beforeClose", event ) === false ) { + return; + } + + this._isOpen = false; + this._focusedElement = null; + this._destroyOverlay(); + this._untrackInstance(); + + if ( !this.opener.filter( ":focusable" ).focus().length ) { + + // support: IE9 + // IE9 throws an "Unspecified error" accessing document.activeElement from an