diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..43a9adc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016-2017 Alexander I. Chebykin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/css/core.css b/css/core.css new file mode 100644 index 0000000..eb22621 --- /dev/null +++ b/css/core.css @@ -0,0 +1,430 @@ +/* Common classes block */ +.wide {width: 100%;} +.hidden { +/* visibility: hidden; */ + display: none; +} +.div_clear { + clear: both; + height: 1px; + margin: 5px 0; +} +.div_maximized { + width: calc(100% - 10px) !important; + height: calc(100% - 90px) !important; +} +.div_maximized_content { + max-height: calc(100% - 30px) !important; +} +.loading_div { + background: url(../gfx/ico_loading.gif) no-repeat center center; +} +.slightly_visible { + opacity: 0.25; +} +th, +.table_header { + font-weight: bold; +} + +.top_right_hint { + text-align: right; + font-weight: normal; + font-style: italic +} + +.distro_logo { + max-height: 140px; + max-width: 380px; +} + +/* Elements block */ +html, body { + margin: 0; + padding: 0; + min-width: 600px; + height: 100%; + width: 100%; + background: url(../gfx/bg.jpg) repeat fixed; + font-family: Arial, sans-serif; +} + +header { + display: block; + position: fixed; + top: 0; + width: 100%; + color: #fff; + background-color: rgba(0, 0, 0, 0.85); + box-shadow: 0 0 10px rgba(0,0,0,0.5); + padding: 4px; + font-size: 14px; + font-weight: bold; +} + +hr { + margin: 0; + padding: 0; + border: 0; + height: 1px; + background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background: linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); +} + +/* Page block */ +#app_header {min-width: 600px;} +#s_header { + position: absolute; + margin: 14px auto 0 55px; + top: 0; + font-family: Courier; + font-size: 24px; + font-weight: bold; + font-style: italic; +} + +#page_content { + margin: 70px 10px 0 10px; + height: inherit; + text-align: center; +} + +/* Attention block */ +#div_attention { + position: absolute; + right: 100px; + top: 4px; +} +#div_attention img {width: 30px;} +#div_attention_content { + right: 0; + width: 400px; +} +#ul_attention { + display: inline-block; + margin: 2px; + padding: 0; + border-color: #fff; +} +#ul_attention li{ + margin: 1px; + float: left; + position: relative; + list-style: none; +} +#ul_attention a { + font-weight: bold; + color: #fff; + text-decoration: none; + display: block; + padding: 8px 8px 6px 8px; + border: solid 1px transparent; + border-radius: 25px; + transition: all 0.5s ease-out 0.1s; +} +#ul_attention a:hover { + box-shadow: inset 0 0 10px rgba(0,0,0,0.5); +} +#ul_attention .current a, +#ul_attention li:hover a { + background-color: #fff; + border: solid 1px #000; +} +#ul_attention li:hover > #div_attention_content { + visibility: visible; + opacity: 0.95; +} +#ul_attention #div_attention_content { + visibility: hidden; + opacity: 0; + position: absolute; + width: 400px; + min-height: 40px; + max-height: calc(100vh - 80px); + max-width: calc(100vw - 80px); + overflow: auto; + padding: 5px; + border: 1px solid #000; + background: #fff; + color: #000; + z-index: 1000; + margin-top: -1px; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + transition: all 0.5s ease-out 0.1s; +} + +/* Navigation block */ +#ul_menu { + display: inline-block; + margin: 2px; + padding: 0; + border-color: #fff; +} +#ul_menu li { + margin: 1px; + float: left; + position: relative; + list-style: none; +} +#ul_menu a { + font-weight: bold; + color: #fff; + text-decoration: none; + display: block; + padding: 8px 8px 6px 8px; + border: solid 1px transparent; + transition: all 0.5s ease-out 0.1s; +} +#ul_menu a:hover { + box-shadow: inset 0 0 10px rgba(0,0,0,0.5); +} +#ul_menu .current a, +#ul_menu li:hover > a { + background-color: #fff; + border: solid 1px #000; +} +#ul_menu li:hover > #div_menu { + visibility: visible; + opacity: 0.95; +} +#ul_menu li img { + width: 25px; + -webkit-filter: invert(1); + filter: invert(1); +} +#ul_menu li:hover > a img { + -webkit-filter: invert(0) !important; + filter: invert(0) !important; +} +#ul_menu #div_menu { + visibility: hidden; + opacity: 0; + position: absolute; + min-width: 700px; + min-height: 100px; + max-height: calc(100vh - 80px); + max-width: calc(100vw - 80px); + overflow: auto; + padding: 5px; + border: 1px solid #000; + background: #fff; + color: #000; + z-index: 1000; + margin-top: -1px; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + transition: all 0.5s ease-out 0.1s; +} +#ul_menu #div_menu a { + color: #000; + text-decoration: none; + padding: 0; +} + +.div_menu_block { + padding: 0; + overflow: hidden; + position: relative; +} +.div_menu_block_caption { + margin: 5px; +} + +/* Menu item */ +.div_m_item { + float: left; + min-height: 120px; + width: 100px; + margin: 5px; + padding: 5px; + text-align: center; + cursor: pointer; +} +.div_m_item:hover { + background-color: rgba(0, 0, 0, 0.25); + transition: all 0.5s ease-out 0.1s; +} +.div_m_item_footer { + text-align: center; + vertical-align: bottom; +} + +.menu_icon { + height: 64px !important; + width: 64px !important; + border-width: 0; + -webkit-filter: invert(0) !important; + filter: invert(0) !important; +} + +/* Widget div block */ +.div_widget { + border: solid #888 1px; + border-radius: 5px 5px 0 0; + display: inline-block; + padding: 0; + margin: 5px; + vertical-align: top; +/* float: left; */ +/* min-width: 900px; */ + min-height: 100px; + width: 600px; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + background: rgba(255, 255, 255, 0.85); + transition: all 0.5s linear; +} + +.div_widget_type_a { + border: solid #888 1px; + border-radius: 5px 5px 0 0; + display: inline-block; + padding: 0; + margin: 5px; + vertical-align: top; +/* float: left; */ +/* min-width: 900px; */ + min-height: 328px; + width: 400px; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + background: rgba(255, 255, 255, 0.85); + transition: all 0.5s linear; +} + +.div_widget_type_b { + border: solid #888 1px; + border-radius: 5px 5px 0 0; + display: inline-block; + padding: 0; + margin: 5px; + vertical-align: top; +/* float: left; */ +/* min-width: 900px; */ + min-height: 330px; + width: 600px; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + background: rgba(255, 255, 255, 0.85); + transition: all 0.5s linear; +} + +.div_widget_full_width { + border: solid #888 1px; + border-radius: 5px 5px 0 0; + display: inline-block; + padding: 0; + margin: 5px; + vertical-align: top; +/* float: left; */ +/* min-width: 900px; */ + min-height: 330px; + width: calc(100% - 10px); + box-shadow: 0 0 10px rgba(0,0,0,0.5); + background: rgba(255, 255, 255, 0.85); + transition: all 0.5s linear; +} + +.div_w_head { + margin: 0; + padding: 5px; + font-weight: bold; + border-radius: 4px 4px 0 0; + color: #fff; + /* background-color: rgba(10, 110, 180, 0.5); */ + background-color: rgba(0, 0, 0, 0.85); + box-shadow: inset 0 0 4px rgba(0,0,0,0.5); +} + +.div_w_content_container { + overflow: auto; + max-height: 300px; /* 500 */ +} + +/* Info table block */ +/*.tbl_info {float: left !important;} */ +.tbl_info table { + border-spacing: 1px; + width: 100%; +} +.tbl_info th { + color: #fff; + background-color: #555; + font-weight: bold; + padding: 5px; +} +.tbl_info td {padding: 2px 5px;} +.tbl_info .even_line {background-color: #ccc;} + +/* Canvas block */ +.canvas_info { + width: 400px; + min-height: 209px; + background-color: #000; +} +.canvas_legend { + float: left; + font-size: 12px; + margin: 0 5px 5px 5px; +} + +.app_widget { + display: inherit !important; +/* float: inherit !important; */ + height: calc(100% - 100px); + width: calc(100% - 10px) !important; +} +.app_widget .div_w_content, +.app_widget .div_w_content_container { + height: calc(100% - 18px); + overflow: hidden !important; + max-height: inherit !important; +} +.app_widget iframe { + width: 100%; + height: 100%; + border: 0; +} + +/* Language selector */ +input, select {border: solid 1px #999;} + +.select_lang { +/* margin-left: 0 !important; + border: solid 1px #999; + float: right; */ + position : absolute; + top : 22px; + right : 20px; + padding : 0 5px 0 20px; + border : 0; + color : #fff; + background-color : transparent; + -webkit-appearance : none; + -moz-appearance : none; + appearance : none; +} +.select_lang option { + color: #000; + background-color: rgba(255, 255, 255, 0.85); +} +/* +select#gender option[value="male"] { background-image:url(male.png); } +select#gender option[value="female"] { background-image:url(female.png); } +select#gender option[value="others"] { background-image:url(others.png); } +*/ +.input_ru { + background-image: url('../gfx/flags/russian-16x16.png'); + background-repeat: no-repeat; + background-attachment: scroll; + background-position: 2px center; + padding-left: 21px; + margin-left: 1px; +} +.input_en { + background-image: url('../gfx/flags/uk-16x16.png'); + background-repeat: no-repeat; + background-attachment: scroll; + background-position: 2px center; + padding-left: 21px; + margin-left: 1px; +} diff --git a/css/core.min.css b/css/core.min.css new file mode 100644 index 0000000..3582ef1 --- /dev/null +++ b/css/core.min.css @@ -0,0 +1,2 @@ + +.wide{width:100%}.hidden{display:none}.div_clear{clear:both;height:1px;margin:5px 0}.div_maximized{width:calc(100% - 10px)!important;height:calc(100% - 90px)!important}.div_maximized_content{max-height:calc(100% - 30px)!important}.loading_div{background:url(../gfx/ico_loading.gif) no-repeat center center}.slightly_visible{opacity:.25}th,.table_header{font-weight:bold}.top_right_hint{text-align:right;font-weight:normal;font-style:italic}.distro_logo{max-height:140px;max-width:380px}html,body{margin:0;padding:0;min-width:600px;height:100%;width:100%;background:url(../gfx/bg.jpg) repeat fixed;font-family:Arial,sans-serif}header{display:block;position:fixed;top:0;width:100%;color:#fff;background-color:rgba(0,0,0,0.85);box-shadow:0 0 10px rgba(0,0,0,0.5);padding:4px;font-size:14px;font-weight:bold}hr{margin:0;padding:0;border:0;height:1px;background-image:-webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));background-image:-moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));background-image:-ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));background-image:-o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));background:linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0))}#app_header{min-width:600px}#s_header{position:absolute;margin:14px auto 0 55px;top:0;font-family:Courier;font-size:24px;font-weight:bold;font-style:italic}#page_content{margin:70px 10px 0 10px;height:inherit;text-align:center}#div_attention{position:absolute;right:100px;top:4px}#div_attention img{width:30px}#div_attention_content{right:0;width:400px}#ul_attention{display:inline-block;margin:2px;padding:0;border-color:#fff}#ul_attention li{margin:1px;float:left;position:relative;list-style:none}#ul_attention a{font-weight:bold;color:#fff;text-decoration:none;display:block;padding:8px 8px 6px 8px;border:solid 1px transparent;border-radius:25px;transition:all .5s ease-out .1s}#ul_attention a:hover{box-shadow:inset 0 0 10px rgba(0,0,0,0.5)}#ul_attention .current a,#ul_attention li:hover a{background-color:#fff;border:solid 1px #000}#ul_attention li:hover>#div_attention_content{visibility:visible;opacity:.95}#ul_attention #div_attention_content{visibility:hidden;opacity:0;position:absolute;width:400px;min-height:40px;max-height:calc(100vh - 80px);max-width:calc(100vw - 80px);overflow:auto;padding:5px;border:1px solid #000;background:#fff;color:#000;z-index:1000;margin-top:-1px;box-shadow:0 0 10px rgba(0,0,0,0.5);transition:all .5s ease-out .1s}#ul_menu{display:inline-block;margin:2px;padding:0;border-color:#fff}#ul_menu li{margin:1px;float:left;position:relative;list-style:none}#ul_menu a{font-weight:bold;color:#fff;text-decoration:none;display:block;padding:8px 8px 6px 8px;border:solid 1px transparent;transition:all .5s ease-out .1s}#ul_menu a:hover{box-shadow:inset 0 0 10px rgba(0,0,0,0.5)}#ul_menu .current a,#ul_menu li:hover>a{background-color:#fff;border:solid 1px #000}#ul_menu li:hover>#div_menu{visibility:visible;opacity:.95}#ul_menu li img{width:25px;-webkit-filter:invert(1);filter:invert(1)}#ul_menu li:hover>a img{-webkit-filter:invert(0)!important;filter:invert(0)!important}#ul_menu #div_menu{visibility:hidden;opacity:0;position:absolute;min-width:700px;min-height:100px;max-height:calc(100vh - 80px);max-width:calc(100vw - 80px);overflow:auto;padding:5px;border:1px solid #000;background:#fff;color:#000;z-index:1000;margin-top:-1px;box-shadow:0 0 10px rgba(0,0,0,0.5);transition:all .5s ease-out .1s}#ul_menu #div_menu a{color:#000;text-decoration:none;padding:0}.div_menu_block{padding:0;overflow:hidden;position:relative}.div_menu_block_caption{margin:5px}.div_m_item{float:left;min-height:120px;width:100px;margin:5px;padding:5px;text-align:center;cursor:pointer}.div_m_item:hover{background-color:rgba(0,0,0,0.25);transition:all .5s ease-out .1s}.div_m_item_footer{text-align:center;vertical-align:bottom}.menu_icon{height:64px!important;width:64px!important;border-width:0;-webkit-filter:invert(0)!important;filter:invert(0)!important}.div_widget{border:solid #888 1px;border-radius:5px 5px 0 0;display:inline-block;padding:0;margin:5px;vertical-align:top;min-height:100px;width:600px;box-shadow:0 0 10px rgba(0,0,0,0.5);background:rgba(255,255,255,0.85);transition:all .5s linear}.div_widget_type_a{border:solid #888 1px;border-radius:5px 5px 0 0;display:inline-block;padding:0;margin:5px;vertical-align:top;min-height:328px;width:400px;box-shadow:0 0 10px rgba(0,0,0,0.5);background:rgba(255,255,255,0.85);transition:all .5s linear}.div_widget_type_b{border:solid #888 1px;border-radius:5px 5px 0 0;display:inline-block;padding:0;margin:5px;vertical-align:top;min-height:330px;width:600px;box-shadow:0 0 10px rgba(0,0,0,0.5);background:rgba(255,255,255,0.85);transition:all .5s linear}.div_widget_full_width{border:solid #888 1px;border-radius:5px 5px 0 0;display:inline-block;padding:0;margin:5px;vertical-align:top;min-height:330px;width:calc(100% - 10px);box-shadow:0 0 10px rgba(0,0,0,0.5);background:rgba(255,255,255,0.85);transition:all .5s linear}.div_w_head{margin:0;padding:5px;font-weight:bold;border-radius:4px 4px 0 0;color:#fff;background-color:rgba(0,0,0,0.85);box-shadow:inset 0 0 4px rgba(0,0,0,0.5)}.div_w_content_container{overflow:auto;max-height:300px}.tbl_info table{border-spacing:1px;width:100%}.tbl_info th{color:#fff;background-color:#555;font-weight:bold;padding:5px}.tbl_info td{padding:2px 5px}.tbl_info .even_line{background-color:#ccc}.canvas_info{width:400px;min-height:209px;background-color:#000}.canvas_legend{float:left;font-size:12px;margin:0 5px 5px 5px}.app_widget{display:inherit!important;height:calc(100% - 100px);width:calc(100% - 10px)!important}.app_widget .div_w_content,.app_widget .div_w_content_container{height:calc(100% - 18px);overflow:hidden!important;max-height:inherit!important}.app_widget iframe{width:100%;height:100%;border:0}input,select{border:solid 1px #999}.select_lang{position:absolute;top:22px;right:20px;padding:0 5px 0 20px;border:0;color:#fff;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.select_lang option{color:#000;background-color:rgba(255,255,255,0.85)}.input_ru{background-image:url('../gfx/flags/russian-16x16.png');background-repeat:no-repeat;background-attachment:scroll;background-position:2px center;padding-left:21px;margin-left:1px}.input_en{background-image:url('../gfx/flags/uk-16x16.png');background-repeat:no-repeat;background-attachment:scroll;background-position:2px center;padding-left:21px;margin-left:1px} \ No newline at end of file diff --git a/gfx/bg.jpg b/gfx/bg.jpg new file mode 100644 index 0000000..2857fb2 Binary files /dev/null and b/gfx/bg.jpg differ diff --git a/gfx/buttons/delete.png b/gfx/buttons/delete.png new file mode 100644 index 0000000..6883ea5 Binary files /dev/null and b/gfx/buttons/delete.png differ diff --git a/gfx/buttons/hires/delete.png b/gfx/buttons/hires/delete.png new file mode 100644 index 0000000..c9b7d18 Binary files /dev/null and b/gfx/buttons/hires/delete.png differ diff --git a/gfx/distros/apple.png b/gfx/distros/apple.png new file mode 100644 index 0000000..f2a84f6 Binary files /dev/null and b/gfx/distros/apple.png differ diff --git a/gfx/distros/arch.png b/gfx/distros/arch.png new file mode 100644 index 0000000..f993c49 Binary files /dev/null and b/gfx/distros/arch.png differ diff --git a/gfx/distros/bsd.png b/gfx/distros/bsd.png new file mode 100644 index 0000000..e417e8d Binary files /dev/null and b/gfx/distros/bsd.png differ diff --git a/gfx/distros/debian.png b/gfx/distros/debian.png new file mode 100644 index 0000000..c98ee4c Binary files /dev/null and b/gfx/distros/debian.png differ diff --git a/gfx/distros/fedora.png b/gfx/distros/fedora.png new file mode 100644 index 0000000..74a23bf Binary files /dev/null and b/gfx/distros/fedora.png differ diff --git a/gfx/distros/hires/apple.png b/gfx/distros/hires/apple.png new file mode 100644 index 0000000..77fd173 Binary files /dev/null and b/gfx/distros/hires/apple.png differ diff --git a/gfx/distros/hires/arch.png b/gfx/distros/hires/arch.png new file mode 100644 index 0000000..3c3d515 Binary files /dev/null and b/gfx/distros/hires/arch.png differ diff --git a/gfx/distros/hires/bsd.png b/gfx/distros/hires/bsd.png new file mode 100644 index 0000000..cef915d Binary files /dev/null and b/gfx/distros/hires/bsd.png differ diff --git a/gfx/distros/hires/debian.png b/gfx/distros/hires/debian.png new file mode 100644 index 0000000..6b68bfa Binary files /dev/null and b/gfx/distros/hires/debian.png differ diff --git a/gfx/distros/hires/fedora.png b/gfx/distros/hires/fedora.png new file mode 100644 index 0000000..089c6c7 Binary files /dev/null and b/gfx/distros/hires/fedora.png differ diff --git a/gfx/distros/hires/linux.svg.png b/gfx/distros/hires/linux.svg.png new file mode 100644 index 0000000..cba8d49 Binary files /dev/null and b/gfx/distros/hires/linux.svg.png differ diff --git a/gfx/distros/hires/opensuse.png b/gfx/distros/hires/opensuse.png new file mode 100644 index 0000000..5a36304 Binary files /dev/null and b/gfx/distros/hires/opensuse.png differ diff --git a/gfx/distros/hires/ubuntu.png b/gfx/distros/hires/ubuntu.png new file mode 100644 index 0000000..06bad4d Binary files /dev/null and b/gfx/distros/hires/ubuntu.png differ diff --git a/gfx/distros/linux.png b/gfx/distros/linux.png new file mode 100644 index 0000000..1dc078c Binary files /dev/null and b/gfx/distros/linux.png differ diff --git a/gfx/distros/opensuse.png b/gfx/distros/opensuse.png new file mode 100644 index 0000000..9bd0119 Binary files /dev/null and b/gfx/distros/opensuse.png differ diff --git a/gfx/distros/ubuntu.png b/gfx/distros/ubuntu.png new file mode 100644 index 0000000..02542f4 Binary files /dev/null and b/gfx/distros/ubuntu.png differ diff --git a/gfx/distros/unknown.png b/gfx/distros/unknown.png new file mode 100644 index 0000000..9e34f91 Binary files /dev/null and b/gfx/distros/unknown.png differ diff --git a/gfx/flags/russian-16x16.png b/gfx/flags/russian-16x16.png new file mode 100644 index 0000000..c6ea9ba Binary files /dev/null and b/gfx/flags/russian-16x16.png differ diff --git a/gfx/flags/uk-16x16.png b/gfx/flags/uk-16x16.png new file mode 100644 index 0000000..562a831 Binary files /dev/null and b/gfx/flags/uk-16x16.png differ diff --git a/gfx/hires/menu.png b/gfx/hires/menu.png new file mode 100644 index 0000000..ec7942c Binary files /dev/null and b/gfx/hires/menu.png differ diff --git a/gfx/icons/about.png b/gfx/icons/about.png new file mode 100644 index 0000000..c018512 Binary files /dev/null and b/gfx/icons/about.png differ diff --git a/gfx/icons/attention.png b/gfx/icons/attention.png new file mode 100644 index 0000000..b745b7c Binary files /dev/null and b/gfx/icons/attention.png differ diff --git a/gfx/icons/help.png b/gfx/icons/help.png new file mode 100644 index 0000000..589db23 Binary files /dev/null and b/gfx/icons/help.png differ diff --git a/gfx/icons/hires/ico-about.png b/gfx/icons/hires/ico-about.png new file mode 100644 index 0000000..c79035a Binary files /dev/null and b/gfx/icons/hires/ico-about.png differ diff --git a/gfx/icons/hires/ico-attention.png b/gfx/icons/hires/ico-attention.png new file mode 100644 index 0000000..e53ba26 Binary files /dev/null and b/gfx/icons/hires/ico-attention.png differ diff --git a/gfx/icons/hires/ico-help.png b/gfx/icons/hires/ico-help.png new file mode 100644 index 0000000..ac02cbd Binary files /dev/null and b/gfx/icons/hires/ico-help.png differ diff --git a/gfx/icons/hires/ico-help2.png b/gfx/icons/hires/ico-help2.png new file mode 100644 index 0000000..ba0a7ee Binary files /dev/null and b/gfx/icons/hires/ico-help2.png differ diff --git a/gfx/icons/hires/ico-network.png b/gfx/icons/hires/ico-network.png new file mode 100644 index 0000000..e2e705c Binary files /dev/null and b/gfx/icons/hires/ico-network.png differ diff --git a/gfx/icons/hires/ico-reboot.png b/gfx/icons/hires/ico-reboot.png new file mode 100644 index 0000000..f0edeab Binary files /dev/null and b/gfx/icons/hires/ico-reboot.png differ diff --git a/gfx/icons/hires/ico-reboot2.png b/gfx/icons/hires/ico-reboot2.png new file mode 100644 index 0000000..db58b61 Binary files /dev/null and b/gfx/icons/hires/ico-reboot2.png differ diff --git a/gfx/icons/hires/ico-server.png b/gfx/icons/hires/ico-server.png new file mode 100644 index 0000000..8fbb7d4 Binary files /dev/null and b/gfx/icons/hires/ico-server.png differ diff --git a/gfx/icons/hires/ico-settings.png b/gfx/icons/hires/ico-settings.png new file mode 100644 index 0000000..caa23a8 Binary files /dev/null and b/gfx/icons/hires/ico-settings.png differ diff --git a/gfx/icons/hires/ico-settings2.png b/gfx/icons/hires/ico-settings2.png new file mode 100644 index 0000000..6bf9576 Binary files /dev/null and b/gfx/icons/hires/ico-settings2.png differ diff --git a/gfx/icons/hires/ico-shutdown.png b/gfx/icons/hires/ico-shutdown.png new file mode 100644 index 0000000..ed1cb2a Binary files /dev/null and b/gfx/icons/hires/ico-shutdown.png differ diff --git a/gfx/icons/hires/ico-storage.png b/gfx/icons/hires/ico-storage.png new file mode 100644 index 0000000..8ce9ada Binary files /dev/null and b/gfx/icons/hires/ico-storage.png differ diff --git a/gfx/icons/hires/ico-sysinfo.png b/gfx/icons/hires/ico-sysinfo.png new file mode 100644 index 0000000..54896be Binary files /dev/null and b/gfx/icons/hires/ico-sysinfo.png differ diff --git a/gfx/icons/hires/ico_loading.gif b/gfx/icons/hires/ico_loading.gif new file mode 100644 index 0000000..a718bd8 Binary files /dev/null and b/gfx/icons/hires/ico_loading.gif differ diff --git a/gfx/icons/hires/network2.png b/gfx/icons/hires/network2.png new file mode 100644 index 0000000..f457b13 Binary files /dev/null and b/gfx/icons/hires/network2.png differ diff --git a/gfx/icons/hires/samba.png b/gfx/icons/hires/samba.png new file mode 100644 index 0000000..178e6d2 Binary files /dev/null and b/gfx/icons/hires/samba.png differ diff --git a/gfx/icons/loading.gif b/gfx/icons/loading.gif new file mode 100644 index 0000000..a718bd8 Binary files /dev/null and b/gfx/icons/loading.gif differ diff --git a/gfx/icons/network.png b/gfx/icons/network.png new file mode 100644 index 0000000..190ec6c Binary files /dev/null and b/gfx/icons/network.png differ diff --git a/gfx/icons/reboot.png b/gfx/icons/reboot.png new file mode 100644 index 0000000..05067f1 Binary files /dev/null and b/gfx/icons/reboot.png differ diff --git a/gfx/icons/samba.png b/gfx/icons/samba.png new file mode 100644 index 0000000..4d2c146 Binary files /dev/null and b/gfx/icons/samba.png differ diff --git a/gfx/icons/server.png b/gfx/icons/server.png new file mode 100644 index 0000000..0cac63b Binary files /dev/null and b/gfx/icons/server.png differ diff --git a/gfx/icons/settings.png b/gfx/icons/settings.png new file mode 100644 index 0000000..f467136 Binary files /dev/null and b/gfx/icons/settings.png differ diff --git a/gfx/icons/shutdown.png b/gfx/icons/shutdown.png new file mode 100644 index 0000000..95f6529 Binary files /dev/null and b/gfx/icons/shutdown.png differ diff --git a/gfx/icons/storage.png b/gfx/icons/storage.png new file mode 100644 index 0000000..fb3b77c Binary files /dev/null and b/gfx/icons/storage.png differ diff --git a/gfx/icons/sysinfo.png b/gfx/icons/sysinfo.png new file mode 100644 index 0000000..708ea36 Binary files /dev/null and b/gfx/icons/sysinfo.png differ diff --git a/gfx/menu.png b/gfx/menu.png new file mode 100644 index 0000000..5524435 Binary files /dev/null and b/gfx/menu.png differ diff --git a/index.dev.html b/index.dev.html new file mode 100644 index 0000000..bb4903d --- /dev/null +++ b/index.dev.html @@ -0,0 +1,38 @@ + + + + Control panel + + + + + +
+ +
Loading...
+ +
+
+ +
+ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..b0f1d39 --- /dev/null +++ b/index.html @@ -0,0 +1,2 @@ + +Control panel
Loading...
diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..e052e16 --- /dev/null +++ b/js/core.js @@ -0,0 +1,2276 @@ +// MIT License +// +// Copyright (c) 2016-2018, Alexander I. Chebykin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * CAI CP v.0.9.1 + * + * @module : Core + * @author : Alexander I. Chebykin + * @copyright : Copyright (c) 2016-2018 Alexander I. Chebykin + * @version : 0.9.1 + * @build date : 2018-06-05 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +(function () { + "use strict"; + + /** + * Return GET parameter + * + * @param {string} key Key name + * + * returns {string} + */ + function $_GET(key) { + var s = window.location.search; + + s = s.match(new RegExp(key + "=([^&=]+)")); + + return s ? s[1] : false; + } + + /** + * Check value is numeric + * + * @param {variant} value Value to be checked + * + * @returns {Boolean} + */ + function isNumeric(value) { + return !isNaN(parseFloat(value)) && isFinite(value); + } + + /** + * Return string with formatted numeric value. ex: 12345,6 -> 12'345,6 + * + * @param {variant} value Source value + * @param {separator} separator Character used to separate thousands + * + * @returns {String} + */ + function numFormatted(value, separator) { + var result = "", + splitted = value.toString().split(" "); + for (var i = 0; i < splitted.length; i++) { + if (isNumeric(splitted[i])) { + result += " " + + splitted[i].toString().replace(/\B(?=(\d{3})+(?!\d))/g, + separator); + } else { + result += " " + splitted[i].toString(); + } + } + return result; + } + + /** + * Add message with timestamp to browser's console + * + * @param {string} log_message Message text + * + * @returns {undefined} + */ + function console_log(log_message) { + var now = new Date(); + console.log(now + ": " + log_message); + } + + /** + * Application constructor + * + * @returns {undefined} + */ + function Application() { + /* Object types */ + this.OT_TABLE_H = 1; + this.OT_TABLE_V = 2; + this.OT_IFRAME = 3; + this.OT_CANVAS = 4; + // this.OT_DIV = 5; + + this.timers = []; + this.apps_list = {}; + this.lang = {}; + this.menu = {}; + this.settings = {}; + this.widgets = {}; + + this.blocks_rotation = ["system", + "system_pulse", + "storage", + "network", + "smb"]; + this.cur_block = 0; + + /* Monitoring variables */ + this.mon_users = 0; + this.mon_users_info = { + "count" : 0, + "last_user": "", + "last_addr": "" + }; + + /* Ctrl pressed flag */ + this.ctrl_on = false; + + /* Down point for swipe event on header */ + this.hdr_x_down = null; + this.hdr_y_down = null; + + this.init(1); + } + + /** + * Add events handlers + * + * @returns {undefined} + */ + Application.prototype.add_evt_handlers = function () { + var app_instance = this; + + /* Add swipe handler to header */ + document.getElementById("app_header").addEventListener("touchstart", + function (event) { + app_instance.hdr_x_down = event.touches[0].clientX; + app_instance.hdr_y_down = event.touches[0].clientY; + }, + false); + document.getElementById("app_header").addEventListener("touchmove", + function (event) { + if (!app_instance.hdr_x_down || !app_instance.hdr_y_down) { + return; + } + var x_up = event.touches[0].clientX, + y_up = event.touches[0].clientY, + delta_x = app_instance.hdr_x_down - x_up, + delta_y = app_instance.hdr_y_down - y_up; + + if (Math.abs(delta_x) > Math.abs(delta_y)) { + if (delta_x > 0) { + /* left swipe */ + app_instance.cur_block += 1; + if (app_instance.cur_block > app_instance.blocks_rotation.length - 1) { + app_instance.cur_block = 0; + } + } else { + /* right swipe */ + app_instance.cur_block -= 1; + if (app_instance.cur_block < 0) { + app_instance.cur_block = app_instance.blocks_rotation.length - 1; + } + } + app_instance.widgets_run(app_instance.blocks_rotation[app_instance.cur_block]); + if (app_instance.blocks_rotation[app_instance.cur_block] === "system") { + app_instance.os_logo_draw(); + } + // } else { + // if (delta_y > 0) { + /* up swipe */ + // } else { + /* down swipe */ + // } + } + /* reset values */ + app_instance.hdr_x_down = null; + app_instance.hdr_y_down = null; + }, + false); + + /* Add ctrl pressed flag switcher */ + document.addEventListener("keydown", function (event) { + if ((event.which === 17) && !app_instance.ctrl_on) { + app_instance.ctrl_on = true; + } + if (app_instance.ctrl_on) { + /* Temporary turn off ctrl flag */ + app_instance.ctrl_on = false; + switch (event.which) { + case 37: // Ctrl + <- + app_instance.cur_block -= 1; + if (app_instance.cur_block < 0) { + app_instance.cur_block = app_instance.blocks_rotation.length - 1; + } + app_instance.widgets_run(app_instance.blocks_rotation[app_instance.cur_block]); + if (app_instance.blocks_rotation[app_instance.cur_block] === "system") { + app_instance.os_logo_draw(); + } + event.preventDefault(); + break; + case 39: // Ctrl + -> + app_instance.cur_block += 1; + if (app_instance.cur_block > app_instance.blocks_rotation.length - 1) { + app_instance.cur_block = 0; + } + app_instance.widgets_run(app_instance.blocks_rotation[app_instance.cur_block]); + if (app_instance.blocks_rotation[app_instance.cur_block] === "system") { + app_instance.os_logo_draw(); + } + event.preventDefault(); + break; + case 49: // Ctrl + 1 + app_instance.widgets_run("system"); + app_instance.os_logo_draw(); + event.preventDefault(); + break; + case 50: // Ctrl + 2 + app_instance.widgets_run("system_pulse"); + event.preventDefault(); + break; + case 51: // Ctrl + 3 + app_instance.widgets_run("storage"); + event.preventDefault(); + break; + case 52: // Ctrl + 4 + app_instance.widgets_run("network"); + event.preventDefault(); + break; + case 53: // Ctrl + 5 + app_instance.widgets_run("smb"); + event.preventDefault(); + break; + case 112: // Ctrl + F1 + app_instance.help_show(false); + event.preventDefault(); + break; + } + + app_instance.ctrl_on = true; + } + }); + document.addEventListener("keyup", function (event) { + if ((event.which === 17) && app_instance.ctrl_on) { + app_instance.ctrl_on = false; + } + }); + }; + + /** + * Get data by uri + * + * @param {string} uri Data uri + * + * returns {undefined} + */ + Application.prototype.get_data = function (uri) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + resolve(this.responseText); + }; + xhr.onerror = reject; + xhr.open("GET", uri); + xhr.send(); + }); + }; + + /** + * Initialize application + * + * @param {numeric} init_level Initialization level: + * 1 - Load settings, + * 2 - Load localization + * 3 - Initializa interface + * + * @returns {Boolean} + */ + Application.prototype.init = function (init_level) { + var app_instance = this, + json_file; + + switch (init_level) { + case 1: + /* Add events handlers */ + this.add_evt_handlers(); + + /* Load user defined settings */ + this.get_data("system/json/settings.json") + .then(function(result) { + app_instance.settings = JSON.parse(result); + + app_instance.init(2); + }) + .catch(function(result) { + console_log("init 1 error: " + result); + }); + + break; + case 2: + /* Load general settings */ + this.get_data("system/json/general_settings/") + .then(function(result) { + var general_settings = JSON.parse(result); + + app_instance.settings.shutdown_enabled = + general_settings.shutdown_enabled; + app_instance.settings.reboot_enabled = + general_settings.reboot_enabled; + app_instance.settings.w_transmission_enabled = + general_settings.w_transmission_enabled; + + app_instance.init(3); + }) + .catch(function(result) { + console_log("init 2 error: " + result); + }); + + break; + case 3: + /* Load locale */ + if ($_GET("lang")) { + json_file = "system/json/locale/" + $_GET("lang") + ".json"; + } else { + json_file = "system/json/locale/" + this.settings.lang + ".json"; + window.history.pushState(null, null, "?lang=" + this.settings.lang); + } + + this.get_data(json_file) + .then(function(result) { + app_instance.lang = JSON.parse(result); + + app_instance.init(4); + }) + .catch(function(result) { + console_log("init 3 error: " + result); + }); + + break; + case 4: + /* Files rights check */ + if (this.settings.check_files_rights) { + this.get_data("system/scripts/check_files.php") + .then(function(result) { + if (JSON.parse(result).length > 0) { + var div_info = document.createElement("div"), + text_info = document.createTextNode(app_instance.lang.fs_errors); + + div_info.style.cursor = "pointer"; + div_info.appendChild(text_info); + div_info.addEventListener("click", function () { + app_instance.page_clear(); + app_instance.widget_run(app_instance.w_table_files_check); + }); + document.getElementById("div_attention_content").appendChild(div_info); + document.getElementById("div_attention").classList.remove("hidden"); + } + + app_instance.init(5); + }) + .catch(function(result) { + console_log("init 4 error: " + result); + }); + } else { + this.init(5); + return; + } + break; + case 5: + /* Interface init */ + /* Menu elements */ + this.menu = { + "div_menu_info": { + "id" : "div_menu_info", + "caption" : this.lang.information, + "visible" : true, + "separator" : false, + "elements": { + "info_system": { + "icon" : "gfx/icons/server.png", + "caption" : this.lang.system, + "callback" : function () { + app_instance.widgets_run("system"); + app_instance.os_logo_draw(); + } + }, + "info_pulse": { + "icon" : "gfx/icons/sysinfo.png", + "caption" : this.lang.system_pulse, + "callback" : function () { + app_instance.widgets_run("system_pulse"); + } + }, + "info_storage": { + "icon" : "gfx/icons/storage.png", + "caption" : this.lang.storage, + "callback" : function () { + app_instance.widgets_run("storage"); + } + }, + "info_network": { + "icon" : "gfx/icons/network.png", + "caption" : this.lang.network, + "callback" : function () { + app_instance.widgets_run("network"); + } + }, + "info_smb": { + "icon" : "gfx/icons/samba.png", + "caption" : this.lang.smb, + "callback" : function () { + app_instance.widgets_run("smb"); + } + } + } + }, + "div_menu_app": { + "id" : "div_menu_app", + "caption" : this.lang.applications, + "visible" : false, + "separator" : true, + "elements": {} + }, + "div_settings": { + "id" : "div_menu_settings", + "caption" : "", + "visible" : true, + "separator" : true, + "elements" : { + "cp_settings": { + "icon" : "gfx/icons/settings.png", + "caption" : this.lang.settings, + "callback" : function () { + app_instance.settings_show(); + } + }, + "cp_help_about": { + "icon" : "gfx/icons/about.png", + "caption" : this.lang.about, + "callback" : function () { + app_instance.help_show(true); + } + }, + "cp_help": { + "icon" : "gfx/icons/help.png", + "caption" : this.lang.help, + "callback" : function () { + app_instance.help_show(false); + } + } + } + }, + "div_shutdown": { + "id" : "div_menu_shutdown", + "caption" : "", + "visible" : true, + "separator" : true, + "elements": { + "srv_reboot": { + "icon" : "gfx/icons/reboot.png", + "caption" : this.lang.reboot, + "callback" : function () { + app_instance.shutdown(true); + } + }, + "srv_shutdown": { + "icon" : "gfx/icons/shutdown.png", + "caption" : this.lang.shutdown, + "callback" : function () { + app_instance.shutdown(false); + } + } + } + } + }; + if (!this.settings.shutdown_enabled && !this.settings.reboot_enabled) { + delete this.menu.div_shutdown; + } else { + if (!this.settings.shutdown_enabled) { + delete this.menu.div_shutdown.elements.srv_shutdown; + } + if (!this.settings.reboot_enabled) { + delete this.menu.div_shutdown.elements.srv_reboot; + } + } + + /* Widgets declarations */ + this.w_table_drv_temp = { + parent : "page_content", + id : "drv_temp_info", + type : this.OT_TABLE_H, + label : this.lang.drv_temp_info, + class_name : "tbl_info", + json_uri : "system/scripts/?widget=hdd_temp", + raw_num : true, + refresh : 60 + }; + this.w_table_drv_smart = { + parent : "page_content", + id : "drv_smart_info", + type : this.OT_TABLE_H, + label : this.lang.drv_smart_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=hdd_smart", + raq_num : true, + refresh : 3600 + }; + this.w_table_fs_info = { + parent : "page_content", + id : "fs_info", + type : this.OT_TABLE_H, + label : this.lang.fs_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=fs", + raw_num : true, + refresh : 30 + }; + this.w_table_iostat_info = { + parent : "page_content", + id : "iostat_info", + type : this.OT_TABLE_H, + label : this.lang.iostat_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=io_stat", + raw_num : false, + refresh : 30 + }; + this.w_table_general_info = { + parent : "page_content", + id : "general_info", + type : this.OT_TABLE_V, + label : this.lang.general_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=general_info", + raw_num : true, + refresh : 10 + }; + this.w_table_cpu_info = { + parent : "page_content", + id : "cpu_info", + type : this.OT_TABLE_V, + label : this.lang.cpu_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=cpu_info", + raw_num : false, + refresh : 0 + }; + this.w_table_memory_info = { + parent : "page_content", + id : "memory_info", + type : this.OT_TABLE_V, + label : this.lang.memory_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=mem_info", + raw_num : false, + refresh : 0 + }; + this.w_table_processes = { + parent : "page_content", + id : "proc_info", + type : this.OT_TABLE_H, + label : this.lang.proc_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=processes", + raw_num : false, + refresh : 15 + }; + this.w_table_arp_cache = { + parent : "page_content", + id : "net_arp_cache_info", + type : this.OT_TABLE_H, + label : this.lang.net_arp_cache_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=arp_cache", + raw_num : true, + refresh : 0 + }; + this.w_table_ip_addr = { + parent : "page_content", + id : "net_ip_addr_info", + type : this.OT_TABLE_H, + label : this.lang.net_ip_addr_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=ip_addr", + raw_num : true, + refresh : 0 + }; + this.w_table_ip_route = { + parent : "page_content", + id : "net_ip_route_info", + type : this.OT_TABLE_H, + label : this.lang.net_ip_route_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=ip_route", + raw_num : true, + refresh : 0 + }; + this.w_table_net_band = { + parent : "page_content", + id : "net_band_info", + type : this.OT_TABLE_H, + label : this.lang.net_band_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=net_band", + raw_num : false, + refresh : 15 + }; + this.w_table_net_conn = { + parent : "page_content", + id : "net_conn_info", + type : this.OT_TABLE_H, + label : this.lang.net_conn_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=net_conn", + raw_num : true, + refresh : 15 + }; + this.w_table_net_mcast = { + parent : "page_content", + id : "net_mcast_info", + type : this.OT_TABLE_H, + label : this.lang.net_mcast_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=net_multicast", + raw_num : true, + refresh : 0 + }; + this.w_table_lstn_socks = { + parent : "page_content", + id : "net_lstn_socks_info", + type : this.OT_TABLE_H, + label : this.lang.net_lstn_socks_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=listen_socks", + raw_num : true, + refresh : 0 + }; + this.w_table_unix_socks = { + parent : "page_content", + id : "net_unix_socks_info", + type : this.OT_TABLE_H, + label : this.lang.net_unix_socks_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=active_unix_socks", + raw_num : true, + refresh : 0 + }; + this.w_table_transmission = { + parent : "page_content", + id : "transmission_info", + type : this.OT_TABLE_H, + label : this.lang.transmission_info, + class_name : "tbl_info", + json_uri : "system/scripts/json_transmission.php", + raw_num : false, + refresh : 30 + }; + this.w_table_users_online = { + parent : "page_content", + id : "users_online_info", + type : this.OT_TABLE_H, + label : this.lang.users_online_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=users_online", + raw_num : true, + refresh : 30 + }; + this.w_table_smb_shares = { + parent : "page_content", + id : "smb_shares_info", + type : this.OT_TABLE_H, + label : this.lang.smb_shares_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=smb_shares", + raw_num : true, + refresh : 30 + }; + this.w_table_smb_proc = { + parent : "page_content", + id : "smb_proc_info", + type : this.OT_TABLE_H, + label : this.lang.smb_proc_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=smb_proc", + raw_num : true, + refresh : 30 + }; + this.w_table_smb_locks = { + parent : "page_content", + id : "smb_locks_info", + type : this.OT_TABLE_H, + label : this.lang.smb_locks_info, + class_name : "tbl_info", + json_uri : "system/json/?widget=smb_locks", + raw_num : true, + refresh : 30 + }; + this.w_canvas_cpu_graph = { + parent : "page_content", + id : "cpu_graph", + type : this.OT_CANVAS, + label : this.lang.cpu_info, + class_name : "canvas_info", + json_uri : "system/json/?widget=cpu_load", + refresh : 5, + min_val : 0, + max_val : 100, + raw_num : true, + measure : "%" + }; + this.w_canvas_mem_graph = { + parent : "page_content", + id : "mem_graph", + type : this.OT_CANVAS, + label : this.lang.memory_info, + class_name : "canvas_info", + json_uri : "system/json/?widget=mem", + refresh : 5, + min_val : 0, + max_val : "auto", + raw_num : true, + measure : "MB" + }; + this.w_canvas_net_down_graph = { + parent : "page_content", + id : "net_down_graph", + type : this.OT_CANVAS, + label : this.lang.net_down_info, + class_name : "canvas_info", + json_uri : "system/json/?widget=net_download_transfer_rate", + refresh : 5, + min_val : 0, + max_val : "auto", + raw_num : true, + measure : "KB/s" + }; + this.w_canvas_net_up_graph = { + parent : "page_content", + id : "net_up_graph", + type : this.OT_CANVAS, + label : this.lang.net_up_info, + class_name : "canvas_info", + json_uri : "system/json/?widget=net_upload_transfer_rate", + refresh : 5, + min_val : 0, + max_val : "auto", + raw_num : true, + measure : "KB/s" + }; + this.w_canvas_swap_graph = { + parent : "page_content", + id : "swap_graph", + type : this.OT_CANVAS, + label : this.lang.swap_info, + class_name : "canvas_info", + json_uri : "system/json/?widget=swap", + refresh : 5, + min_val : 0, + max_val : "auto", + raw_num : true, + measure : "MB" + }; + + this.w_table_files_check = { + parent : "page_content", + id : "files_check", + type : this.OT_TABLE_H, + label : this.lang.fs_check, + class_name : "tbl_info", + raw_num : true, + json_uri : "system/scripts/check_files.php" + }; + + this.w_reboot_iframe = { + parent : "page_content", + id : "reboot_iframe", + type : this.OT_IFRAME, + label : this.lang.reboot, + class_name : "app_widget", + json_uri : "system/action/?lang=" + $_GET("lang") + "&do=reboot" + }; + this.w_shutdown_iframe = { + parent : "page_content", + id : "shutdown_iframe", + type : this.OT_IFRAME, + label : this.lang.shutdown, + class_name : "app_widget", + json_uri : "system/action/?lang=" + $_GET("lang") + "&do=shutdown" + }; + + this.w_settings_iframe = { + parent : "page_content", + id : "iframe_settings", + type : this.OT_IFRAME, + label : this.lang.settings, + class_name : "app_widget", + json_uri : "system/ui/forms/settings/?lang=" + $_GET("lang") + }; + + this.w_about_iframe = { + parent : "page_content", + id : "iframe_cp_about", + type : this.OT_IFRAME, + label : this.lang.help, + class_name : "app_widget", + json_uri : "system/help/?lang=" + $_GET("lang") + "#about" + }; + this.w_help_iframe = { + parent : "page_content", + id : "iframe_cp_help", + type : this.OT_IFRAME, + label : this.lang.help, + class_name : "app_widget", + json_uri : "system/help/?lang=" + $_GET("lang") + }; + + /* Widgets lists */ + this.widgets_lists = { + "storage": [ + { + "page_title" : this.lang.storage + }, + this.w_table_drv_temp, + this.w_table_drv_smart, + this.w_table_fs_info, + this.w_table_iostat_info + ], + "network": [ + { + "page_title" : this.lang.network + }, + this.w_canvas_net_down_graph, + this.w_canvas_net_up_graph, + { + "break" : true + }, + this.w_table_ip_addr, + this.w_table_net_band, + this.w_table_ip_route, + this.w_table_users_online, + this.w_table_net_mcast, + this.w_table_arp_cache, + this.w_table_lstn_socks, + { + "widget_css" : "div_widget_full_width" + }, + this.w_table_unix_socks, + { + "widget_css" : "div_widget_full_width" + }, + this.w_table_net_conn, + this.w_table_transmission + ], + "smb": [ + { + "page_title" : this.lang.smb + }, + this.w_table_smb_proc, + this.w_table_smb_shares, + { + "widget_css" : "div_widget_full_width" + }, + this.w_table_smb_locks + ], + "system": [ + { + "page_title" : this.lang.system, + "widget_css" : "div_widget_type_a" + }, + this.w_table_general_info, + this.w_table_memory_info, + this.w_table_cpu_info + ], + "system_pulse": [ + { + "page_title" : this.lang.system_pulse + }, + this.w_canvas_cpu_graph, + this.w_canvas_mem_graph, + this.w_canvas_swap_graph, + this.w_canvas_net_down_graph, + this.w_canvas_net_up_graph, + { + "widget_css" : "div_widget_full_width" + }, + this.w_table_processes + ] + }; + + this.server_name_fill(); + this.language_selector_add(); + + var m_menu = new MainMenu(this.lang); + + m_menu.clear_menu(m_menu.main_menu); + m_menu.build_menu(this.menu); + + this.apps_list_load(); + // app.check_apps(); + if (window.location.hash.substr(1) !== "") { + if (window.location.hash.substr(1) === "settings") { + this.settings_show(); + } else if (window.location.hash.substr(1) === "cp_help") { + this.help_show(false); + } else if (window.location.hash.substr(1) === "cp_about") { + this.help_show(true); + } else if (window.location.hash.substr(1, 3) !== "app") { + this.widgets_run(window.location.hash.substr(1)); + if (window.location.hash.substr(1) === "system") { + this.os_logo_draw(); + } + } else { + this.app_execute(window.location.hash.substr(1)); + } + } else { + this.widgets_run("system"); + this.os_logo_draw(); + } + + /* Enable S.M.A.R.T and temperature monitoring */ + this.monitoring_enable(); + + return true; + } + }; + + /** + * Add language selector to page's header + * + * @returns {undefined} + */ + Application.prototype.language_selector_add = function () { + var header = document.getElementById("app_header"), + select_lang = document.createElement("select"); + + select_lang.id = "lang"; + select_lang.classList.add("select_lang"); + + if ($_GET("lang")) { + select_lang.classList.add("input_" + $_GET("lang")); + } else { + select_lang.classList.add("input_" + this.settings.lang); + } + + select_lang.onchange = function () { + /* + * window.open(URL,name,specs,replace): where name: + * + * _blank - URL is loaded into a new window. This is default + * _parent - URL is loaded into the parent frame + * _self - URL replaces the current page + * _top - URL replaces any framesets that may be loaded + * name - The name of the window (Note: the name does not specify + * the title of the new window) + */ + window.open(this.options[this.selectedIndex].value, "_self"); + }; + + for (var language in this.settings.langs) { + if (this.settings.langs.hasOwnProperty(language)) { + var opt_lang = document.createElement("option"); + + opt_lang.value = "?lang=" + language; + opt_lang.appendChild(document.createTextNode(this.settings.langs[language])); + + if ($_GET("lang") && $_GET("lang") === language) { + opt_lang.selected = true; + } else if (!$_GET("lang") && this.settings.lang === language){ + opt_lang.selected = true; + } + + select_lang.appendChild(opt_lang); + } + } + header.appendChild(select_lang); + }; + + /** + * Change page title + * + * @param {string} new_title New page title + * + * @returns {undefined} + */ + Application.prototype.page_title_change = function (new_title) { + var srv_title = document.getElementById("s_header").textContent; + + document.title = document.getElementById("s_header").textContent = + srv_title.substring(0, srv_title.indexOf("#") + 1) + " " + new_title; + }; + + /** + * Check application existance and remove from menu if unexisted + * + * @param {string} app_name Application name + * @param {string} app_exe Application executable + * + * @returns {undefined} + */ + Application.prototype.app_remove_unexisted = function (app_name, app_exe) { + this.get_data("system/json/?widget=check_app¶m=" + app_exe) + .then(function(result) { + var element = document.getElementById("div_menu_" + app_name), + json_data = JSON.parse(result); + + if ((json_data.exec !== "") && (!json_data.installed)) { + element.parentNode.removeChild(element); + } else { + element.classList.remove("hidden"); + } + }) + .catch(function(result) { + console_log("app_remove_unexisted error: " + result); + }); + }; + + /** + * Draw OS distro logo + * + * @returns {undefined} + */ + Application.prototype.os_logo_draw = function () { + this.get_data("system/json/?widget=os_distr") + .then(function(result) { + var json_data = JSON.parse(result), + div_gi = document.getElementById("general_info"); + + if (!div_gi) { + return; + } + + var img_logo = document.createElement("img"); + + img_logo.classList.add("distro_logo"); + + if (json_data.Distr.match(/opensuse/i)) { + img_logo.src = "gfx/distros/opensuse.png"; + } else if (json_data.Distr.match(/arch/i)) { + img_logo.src = "gfx/distros/arch.png"; + } else if (json_data.Distr.match(/bsd/i)) { + img_logo.src = "gfx/distros/bsd.png"; + } else if (json_data.Distr.match(/debian/i)) { + img_logo.src = "gfx/distros/debian.png"; + } else if (json_data.Distr.match(/fedora/i)) { + img_logo.src = "gfx/distros/fedora.png"; + } else if (json_data.Distr.match(/ubuntu/i)) { + img_logo.src = "gfx/distros/ubuntu.png"; + } else if (json_data.Family.match(/linux/i)) { + img_logo.src = "gfx/distros/linux.png"; + } else if (json_data.Family.match(/darwin/i)) { + img_logo.src = "gfx/distros/apple.png"; + } else { + img_logo.src = "gfx/distros/unknown.png"; + } + + div_gi.appendChild(img_logo); + }) + .catch(function(result) { + console_log("Can't get disto info: " + result); + }); + }; + + /** + * Clear page content + * + * returns {undefined} + */ + Application.prototype.page_clear = function () { + var parent_node = document.getElementById("page_content"); + + /* Clear timers */ + for (var key in this.timers) { + if (this.timers.hasOwnProperty(key)) { + clearInterval(this.timers[key]); + } + } + this.timers = []; + + /* Remove widgets */ + while (parent_node.firstChild) { + parent_node.removeChild(parent_node.firstChild); + } + }; + + /** + * Open control panel with selected application or widgets screen in new + * window + * + * @param {string} hash URL hash + * + * @returns {undefined} + */ + Application.prototype.app_new_window = function (hash) { + var new_location = location.href.replace(location.hash,""), + new_win = window.open(new_location + "#" + hash, "_blank"); + + new_win.focus(); + + return; + }; + + /** + * Draw application widget + * + * @param {object} w_info Widget info + * + * @returns {undefined} + */ + Application.prototype.app_widget_draw = function (w_info) { + var app_hash = w_info.id.substring(7); + + if (this.ctrl_on) { + this.app_new_window(app_hash); + return; + } + + var app_widget = new Widget(this, w_info); + + this.page_clear(); + this.page_title_change(w_info.label); + + app_widget.draw(); + + location.hash = app_hash; + + document.getElementById(w_info.id).classList.remove("slightly_visible"); + }; + + /** + * Fill server name on HTML page + * + * @returns {undefined} + */ + Application.prototype.server_name_fill = function () { + this.get_data("system/json/?widget=srv_name") + .then(function(result) { + var s_header = document.getElementById("s_header"), + json_data = JSON.parse(result); + + if (s_header.textContent !== "Loading...") { + var temp = s_header.textContent; + + s_header.removeChild(s_header.childNodes[0]); + s_header.appendChild(document.createTextNode( + json_data.server_name + " #" + temp)); + } else { + s_header.removeChild(s_header.childNodes[0]); + s_header.appendChild(document.createTextNode( + json_data.server_name + " #")); + } + + document.title = s_header.textContent; + }) + .catch(function(result) { + console_log("server_name_fill error: " + result); + }); + }; + + /** + * Execute application + * + * @param {string} app_name application name + * + * @returns {undefined} + */ + Application.prototype.app_execute = function (app_name) { + var app_instance = this; + + this.get_data("system/apps/" + app_name.substr(4) + "/app.json") + .then(function(result) { +// TODO: Check why https_uri + var json_data = JSON.parse(result), + uri = json_data.https_uri, + app_iframe = { + parent : "page_content", + id : "iframe_" + json_data.app_name, + type : app_instance.OT_IFRAME, + label : json_data.caption, + class_name : "app_widget", + json_uri : uri.replace("[server_name]", + window.location.hostname) + }; + + app_instance.app_widget_draw(app_iframe); + }) + .catch(function(result) { + console_log("app_execute error: " + result); + }); + }; + + /** + * Enable HDD S.M.A.R.T. monitoring + * + * @param {numeric} timeout Timeout in seconds + * + * @returns {undefined} + */ + Application.prototype.monitor_hdd_smart = function (timeout) { + var app_instance = this; + + setTimeout(function () { + app_instance.get_data("system/json/?widget=hdd_smart") + .then(function(result) { + var hdd_smart = JSON.parse(result), + cur_date = new Date(); + + for (var hdd in hdd_smart) { + if ((hdd_smart[hdd].status.toLowerCase() !== "passed") && + (hdd_smart[hdd].status.toLowerCase() !== "ok")) { + app_instance.notify(app_instance.lang.attention, + {body: cur_date.toLocaleString() + ": " + + app_instance.lang.hdd_smart_error + " " + + hdd_smart[hdd].drive + " (" + + hdd_smart[hdd].status + ")", + tag: "cai_cp", + icon: "../gfx/icons/attention.png", + dir: "auto"}, + function () { + app_instance.widgets_run("storage"); + }); + } + } + + console_log("HDD SMART monitor"); + app_instance.monitor_hdd_smart(timeout); + }) + .catch(function(result) { + console_log("monitor_hdd_smart error: " + result); + app_instance.monitor_hdd_smart(timeout); + }); + }, + timeout * 1000); + }; + + /** + * Enable HDD temperature monitoring + * + * @param {numeric} timeout Timeout in seconds + * + * @returns {undefined} + */ + Application.prototype.monitor_hdd_temp = function (timeout) { + var app_instance = this; + + setTimeout(function () { + app_instance.get_data("system/scripts/?widget=hdd_temp") + .then(function(result) { + var hdd_temp = JSON.parse(result), + cur_date = new Date(); + + for (var hdd in hdd_temp) { + if (hdd_temp[hdd].temperature.substring(0, + hdd_temp[hdd].temperature.length - 2) > app_instance.settings.max_hdd_temp) { + app_instance.notify(app_instance.lang.attention, + {body: cur_date.toLocaleString() + ": " + + app_instance.lang.hdd_overheat + " " + + hdd_temp[hdd].drive + " (" + + hdd_temp[hdd].model + "): " + + hdd_temp[hdd].temperature, + tag: "cai_cp", + icon: "../gfx/icons/attention.png", + dir: "auto"}, + function () { + app_instance.widgets_run("storage"); + }); + } + } + + console_log("HDD temperature monitor"); + app_instance.monitor_hdd_temp(timeout); + }) + .catch(function(result) { + console_log("monitor_hdd_temp error: " + result); + app_instance.monitor_hdd_temp(timeout); + }); + }, + timeout * 1000); + }; + + /** + * Enable online users monitoring + * + * @param {numeric} timeout Timeout in seconds + * + * @returns {undefined} + */ + Application.prototype.monitor_users_online = function (timeout) { + var app_instance = this; + + setTimeout(function () { + app_instance.get_data("system/scripts/?widget=users_online") + .then(function(result) { + var users_online = JSON.parse(result), + cur_date = new Date(); + + if (users_online.length > app_instance.mon_users_info.count || + users_online[users_online.length - 1].from !== app_instance.mon_users_info.last_addr || + users_online[users_online.length - 1].user !== app_instance.mon_users_info.last_user) { + app_instance.notify(app_instance.lang.attention, + {body: cur_date.toLocaleString() + ": " + + app_instance.lang.new_user_connected + + " (" + + users_online[users_online.length - 1].user + + ", " + + users_online[users_online.length - 1].from + + ")", + tag: "cai_cp", + icon: "../gfx/icons/attention.png", + dir: "auto"}, + function () { + app_instance.widgets_run("network"); + }); + } + app_instance.mon_users_info.count = users_online.length; + app_instance.mon_users_info.last_user = users_online[users_online.length - 1].user; + app_instance.mon_users_info.last_addr = users_online[users_online.length - 1].from; + + console_log("Online users monitor"); + app_instance.monitor_users_online(timeout); + }) + .catch(function(result) { + console_log("monitor_users_online error: " + result); + app_instance.monitor_users_online(timeout); + }); + }, + timeout * 1000); + }; + + /** + * Enable monitoring functions + * + * @returns {undefined} + */ + Application.prototype.monitoring_enable = function () { + /* Set HDD temperature monitoring */ + if (isNaN(this.settings.check_hdd_temp_interval)) { + this.settings.check_hdd_temp_interval = 0; + } else { + this.monitor_hdd_temp(this.settings.check_hdd_temp_interval); + } + + /* Set S.M.A.R.T. monitoring */ + if (isNaN(this.settings.check_smart_interval)) { + this.settings.check_smart_interval = 0; + } else { + this.monitor_hdd_smart(this.settings.check_smart_interval); + } + + /* Set connected users monitoring */ + if (isNaN(this.settings.check_users_online_interval)) { + this.settings.check_users_online_interval = 0; + } else { + this.monitor_users_online(this.settings.check_users_online_interval); + } + }; + + /** + * Show browser notification + * + * @param {string} title Notigication title + * @param {object} options Notification options + * @param {function} callback Callback function + * + * @returns {undefined} + */ + Application.prototype.notify = function (title, options, callback) { + if (!("Notification" in window) || + (Notification.permission.toLowerCase() === "denied")) { + /* Notifications not supported or denied */ + return; + } + + if (Notification.permission === "granted") { // Notifications allowed + var notification = new Notification(title, options); + + notification.onclick = callback;/* function () { + console.log("notification clicked"); + }; */ + } else { // Trying to get permissions + Notification.requestPermission(function (permission) { + if (permission === "granted") { + var notification = new Notification(title, options); + } /*else { + console.log("notifications prohibited"); + } */ + }); + } + }; + + /** + * Register applications in menu + * + * @returns {undefined} + */ + Application.prototype.apps_register = function () { + var m_menu = new MainMenu(this.lang); + + for (var key in this.apps_list) { + if (this.apps_list.hasOwnProperty(key)) { + m_menu.register_app(this, this.apps_list[key]); + document.getElementById("div_menu_app").classList.remove("hidden"); + } + } + }; + + /** + * Load applications list + * + * @returns {undefined} + */ + Application.prototype.apps_list_load = function () { + var app_instance = this; + + this.get_data("system/apps/apps.json") + .then(function(result) { + var json_data = JSON.parse(result); + app_instance.apps_list = json_data; + app_instance.apps_register(); + }) + .catch(function(result) { + console_log("apps_list_load error:" + result); + }); + }; + + /** + * Run widget + * + * @param {object} widget_info Widget description + * + * @returns {undefined} + */ + Application.prototype.widget_run = function (widget_info) { + Object.size = function(obj) { + var size = 0, key; + for (key in obj) { + if (obj.hasOwnProperty(key)) size++; + } + return size; + }; + + var w_index = Object.size(this.widgets); //this.widgets.length; + + this.widgets[w_index] = new Widget(this, widget_info); + this.widgets[w_index].draw(this.widgets[w_index]); + + if (widget_info.type === this.OT_TABLE_H || + widget_info.type === this.OT_TABLE_V) { + this.widgets[w_index].update_table_data(this, w_index); + if (widget_info.refresh > 0) { + this.timers[widget_info.id] = + setInterval(this.widgets[w_index].update_table_data, + widget_info.refresh * 1000, + this, w_index); + } + } else if (widget_info.type === this.OT_CANVAS) { + this.widgets[w_index].update_canvas_data(this, w_index); + if (widget_info.refresh > 0) { + this.timers[widget_info.id] = + setInterval(this.widgets[w_index].update_canvas_data, + widget_info.refresh * 1000, + this, w_index); + } + } + }; + + /** + * Draws application widgets + * + * @param {array} block_name Name of widgets block + * + * @returns {undefined} + */ + Application.prototype.widgets_run = function (block_name) { + /* Open in new window if ctrl is pressed */ + if (this.ctrl_on) { + this.app_new_window(block_name); + return; + } + + if (typeof block_name === "undefined") { + return; + } + + this.cur_block = this.blocks_rotation.indexOf(block_name); + + var class_name = "div_widget", + no_widgets = false; + + this.page_clear(); + + location.hash = block_name; + + this.widgets = {}; + + for (var i = 0; i < this.widgets_lists[block_name].length; i++) { + no_widgets = false; + + if (this.widgets_lists[block_name][i].widget_css !== undefined) { + /* Apply widget css */ + no_widgets = true; + class_name = this.widgets_lists[block_name][i].widget_css; + } else if (this.widgets_lists[block_name][i].break !== undefined && + this.widgets_lists[block_name][i].break === true) { + /* Insert break */ + var div_break = document.createElement("div"); + div_break.className = "div_clear"; + + document.getElementById("page_content").appendChild(div_break); + + no_widgets = true; + } + + if (this.widgets_lists[block_name][i].page_title !== undefined) { + this.page_title_change(this.widgets_lists[block_name][i].page_title); + + no_widgets = true; + } + + /* If widget info */ + if (!no_widgets) { + if (!this.settings.w_transmission_enabled && + this.widgets_lists[block_name][i].id === "transmission_info") { + /* Skip transmission widget if disabled */ + } else { + this.widget_run(this.widgets_lists[block_name][i]); + /* Apply widget style class */ + if (class_name !== "div_widget") { + document.getElementById(this.widgets_lists[block_name][i].id).classList.remove("div_widget"); + document.getElementById(this.widgets_lists[block_name][i].id).classList.add(class_name); + } + } + } + } + }; + + /** + * Show help + * + * @param {bool} about_info Show "About" info + * + * @returns {undefined} + */ + Application.prototype.help_show = function (about_info) { + if (!this.ctrl_on) { + this.page_clear(); + } + + if (about_info) { + this.app_widget_draw(this.w_about_iframe); + } else { + this.app_widget_draw(this.w_help_iframe); + } + }; + + /** + * Show settings form + * + * @returns {undefined} + */ + Application.prototype.settings_show = function () { + if (!this.ctrl_on) { + this.page_clear(); + } + + this.app_widget_draw(this.w_settings_iframe); + }; + + /** + * Shutdown server + * + * @param {bool} reboot Reboot system flag + * + * @returns {undefined} + */ + Application.prototype.shutdown = function (reboot) { + if (reboot && window.confirm (this.lang.reboot_confirm)) { + this.page_clear(); + this.app_widget_draw(this.w_reboot_iframe); + } else if (!reboot && window.confirm (this.lang.shutdown_confirm)) { + this.page_clear(); + this.app_widget_draw(this.w_shutdown_iframe); + } + }; + + /** + * Main menu constructor + * + * @param {string} lang Language + * + * @returns {undefined} + */ + function MainMenu(lang) { + this.main_menu = document.getElementById("div_menu"); + this.info_menu = document.getElementById("div_menu_info"); + this.app_menu = document.getElementById("div_menu_app"); + this.settings_menu = document.getElementById("div_menu_settings"); + this.lang = lang; + } + + /** + * Add icons block to menu + * + * @param {array} block_info Menu block information + * + * @returns {object} Menu block's div + */ + MainMenu.prototype.add_block = function (block_info) { + var div_menu_block = document.createElement("div"), + div_menu_block_caption = document.createElement("div"); + + div_menu_block.id = block_info.id; + div_menu_block.classList.add("div_menu_block"); + + if (!block_info.visible) { + div_menu_block.classList.add("hidden"); + } + + this.main_menu.appendChild(div_menu_block); + + div_menu_block_caption.classList.add("div_menu_block_caption"); + div_menu_block_caption.appendChild(document.createTextNode(block_info.caption)); + div_menu_block.appendChild(div_menu_block_caption); + + if (block_info.separator) { + div_menu_block.appendChild(document.createElement("hr")); + } + + return div_menu_block; + }; + + /** + * Add menu item to menu block + * + * @param {object} parent_block Parent block for icon + * @param {string} id Menu item id + * @param {string} icon_src Path to icon file + * @param {string} caption Menu caption + * @param {bool} hidden Add menu in hidden state + * @param {object} callback_function OnClick callback function + * + * @returns {undefined} + */ + MainMenu.prototype.add_item = function (parent_block, + id, + icon_src, + caption, + hidden, + callback_function) { + var m_item_div = document.createElement("div"), + m_item_img = document.createElement("img"), + m_item_footer_div = document.createElement("div"); + + m_item_div.id = "div_" + id; + m_item_div.classList.add("div_m_item"); + m_item_div.onclick = callback_function; +/* +m_item_div.addEventListener("contextmenu", function(e) { + e.preventDefault(); +}); +*/ + if (hidden) { + m_item_div.classList.add("hidden"); + } + + m_item_img.classList.add("menu_icon"); + m_item_img.src = icon_src; + m_item_div.appendChild(m_item_img); + + m_item_footer_div.classList.add("div_m_item_footer"); + m_item_footer_div.appendChild(document.createTextNode(caption)); + m_item_div.appendChild(m_item_footer_div); + + parent_block.appendChild(m_item_div); + }; + + /** + * Build menu structure + * + * @param {object} menu_items menu structure + * + * @returns {undefined} + */ + MainMenu.prototype.build_menu = function (menu_items) { + var div_ctrl_hint = document.createElement("div"); + + div_ctrl_hint.classList.add("top_right_hint"); + div_ctrl_hint.appendChild(document.createTextNode(this.lang.ctrl_click_hint)); + + this.main_menu.appendChild(div_ctrl_hint); + + for (var menu_block in menu_items) { + if (menu_items.hasOwnProperty(menu_block)) { + var div_menu_block = this.add_block(menu_items[menu_block]); + + for (var menu_item in menu_items[menu_block].elements) { + if (menu_items[menu_block].elements.hasOwnProperty(menu_item)) { + this.add_item( + div_menu_block, + menu_item, + menu_items[menu_block].elements[menu_item].icon, + menu_items[menu_block].elements[menu_item].caption, + false, + menu_items[menu_block].elements[menu_item].callback + ); + } + } + + var div_clear = document.createElement("div"); + + div_clear.classList.add("div_clear"); + div_menu_block.appendChild(div_clear); + + this.main_menu.appendChild(div_menu_block); + } + } + }; + + /** + * Clear menu or submenu + * + * @param {object} menu Menu object to clear + * + * @returns {undefined} + */ + MainMenu.prototype.clear_menu = function (menu) { + while (menu.firstChild) { + menu.removeChild(menu.firstChild); + } + }; + + /** + * Register application in menu + * + * @param {object} app_instance Application instance + * @param {string} app_dir Directory containing app.json file with + * application info + * + * @returns {undefined} + */ + MainMenu.prototype.register_app = function (app_instance, app_dir) { + var menu_obj = this; + + app_instance.get_data("system/apps/" + app_dir + "/app.json") + .then(function(result) { + var json_data = JSON.parse(result), + parent_block = document.getElementById("div_menu_app"), + uri, + app_iframe; + + switch (window.location.protocol) { + case "https:": + if (json_data.use_https && json_data.https_uri !== "") { + if (json_data.frame_support) { + uri = json_data.https_uri; + /* run in frame */ + app_iframe = { + parent : "page_content", + id : "iframe_" + json_data.app_name, + type : app_instance.OT_IFRAME, + label : json_data.caption, + class_name : "app_widget", + json_uri : uri.replace("[server_name]", + window.location.hostname) + }; + menu_obj.add_item(parent_block, + "menu_" + json_data.app_name, + "system/apps/" + app_dir + "/icon.png", + json_data.caption, + json_data.exec !== "", + function () { + app_instance.app_widget_draw(app_iframe); + } + ); + } else { + menu_obj.add_item(parent_block, + "menu_" + json_data.app_name, + "system/apps/" + app_dir + "/icon.png", + json_data.caption, + json_data.exec !== "", + function () { + var uri = json_data.https_uri; + + window.open(uri.replace("[server_name]", + window.location.hostname), + "_blank"); + } + ); + } + } else if (json_data.use_http && json_data.http_uri !== "") { + menu_obj.add_item(parent_block, + "menu_" + json_data.app_name, + "system/apps/" + app_dir + "/icon.png", + json_data.caption, + json_data.exec !== "", + function () { + var uri = json_data.http_uri; + + window.open(uri.replace("[server_name]", + window.location.hostname), + "_blank"); + } + ); + } + break; + case "http:": + if (json_data.use_http && json_data.http_uri !== "") { + if (json_data.frame_support) { + uri = json_data.http_uri; + /* run in frame */ + app_iframe = { + parent : "page_content", + id : "iframe_" + json_data.app_name, + type : app_instance.OT_IFRAME, + label : json_data.caption, + class_name : "app_widget", + json_uri : uri.replace("[server_name]", + window.location.hostname) + }; + menu_obj.add_item(parent_block, + "menu_" + json_data.app_name, + "system/apps/" + app_dir + "/icon.png", + json_data.caption, + json_data.exec !== "", + function () { + app_instance.app_widget_draw(app_iframe); + } + ); + } else { + menu_obj.add_item(parent_block, + "menu_" + json_data.app_name, + "system/apps/" + app_dir + "/icon.png", + json_data.caption, + json_data.exec !== "", + function () { + var uri = json_data.http_uri; + + window.open(uri.replace("[server_name]", + window.location.hostname), + "_blank"); + } + ); + } + } else if (json_data.use_https && json_data.https_uri !== "") { + menu_obj.add_item(parent_block, + "menu_" + json_data.app_name, + "system/apps/" + app_dir + "/icon.png", + json_data.caption, + json_data.exec !== "", + function () { + var uri = json_data.https_uri; + + window.open(uri.replace("[server_name]", + window.location.hostname), + "_blank"); + } + ); + } + break; + } + for (var i = 0; i < json_data.require.length; i++) { + app_instance.app_remove_unexisted(json_data.app_name, + json_data.require[i].data); + } + }) + .catch(function(result) { + console_log("register_app error: " + result); + }); + }; + + /** + * Widgets constructor + * + * @param {object} caller Caller object + * @param {array} w_info Array with Widget information + * @param {string} w_data Widget data + * + * @returns {undefined} + */ + function Widget(caller, w_info, w_data) { + this.parent_app = caller; + this.info = w_info; + this.data = w_data || null; + this.parent_el = document.getElementById(w_info.parent); + + /* Double tap on header handler vars */ + this.dt_hdr_timeout = null; + this.dt_hdr_tap_time = 0; + } + + /** + * Toggle maximized widget state + * + * @returns {undefined} + */ + Widget.prototype.toggle_maximized = function () { + var parent_node = document.getElementById(this.parentNode.id); + + if (parent_node.classList.contains("canvas_info")) { + return; + } + + document.getElementById(this.parentNode.id).classList.toggle("div_maximized"); + + for (var i = 0; i < parent_node.childNodes.length; i++) { + if (parent_node.childNodes[i].classList.contains("div_w_content_container")) { + parent_node.childNodes[i].classList.toggle("div_maximized_content"); + } + } + }; + + /** + * Draw widget + * + * @returns {undefined} + */ + Widget.prototype.draw = function () { + var widget_instance = this, + widget_node, + widget_c_container; + + if (document.getElementById(this.info.id) !== null) { + widget_node = document.getElementById(this.info.id); + + widget_c_container = widget_node.childNodes[1]; + widget_c_container.removeChild(widget_c_container.childNodes[0]); + } else { + var widget_head = document.createElement("div"); + + widget_node = document.createElement("div"); + widget_node.classList.add("div_widget"); + + if (this.parent_app.settings.dim_on_create) { + widget_node.classList.add("slightly_visible"); + } + + widget_node.id = this.info.id; + + if (this.info.class_name !== "") { + widget_node.classList.add(this.info.class_name); + } + + widget_head.classList.add("div_w_head"); + widget_head.appendChild(document.createTextNode(this.info.label)); + widget_head.addEventListener("dblclick", this.toggle_maximized); + /* Add double tap event */ + widget_head.addEventListener("touchend", function(event) { + var cur_time = new Date().getTime(), + tap_len = cur_time - this.dt_hdr_tap_time; + + clearTimeout(widget_instance.dt_hdr_timeout); + + if (tap_len < 500 && tap_len > 0) { + event.preventDefault(); + widget_instance.toggle_maximized(); + } else { + widget_instance.dt_hdr_timeout = setTimeout(function() { + clearTimeout(widget_instance.dt_hdr_timeout); + }, 500); + } + + widget_instance.dt_hdr_tap_time = cur_time; + }); + + widget_c_container = document.createElement("div"); + widget_c_container.classList.add("div_w_content_container"); + + widget_node.appendChild(widget_head); + widget_node.appendChild(widget_c_container); + + this.parent_el.appendChild(widget_node); + } + + var widget_content = document.createElement("div"); + + widget_content.classList.add("div_w_content"); + + widget_c_container.appendChild(widget_content); + + switch (this.info.type) { + case this.parent_app.OT_TABLE_H: + case this.parent_app.OT_TABLE_V: + var table_node = this.html_table(this.info.type === this.parent_app.OT_TABLE_V); + + if (this.info.class_name !== "") { + table_node.classList.add(this.info.class_name); + } + widget_content.appendChild(table_node); + + break; + case this.parent_app.OT_IFRAME: + var iframe_node = document.createElement("iframe"); + + iframe_node.src = this.info.json_uri; + widget_content.appendChild(iframe_node); + + break; + case this.parent_app.OT_CANVAS: + var canvas_node = document.createElement("canvas"), + s_settings = {interpolation: this.info.interpolation, + //interpolation : "linear", + //interpolation : "step", + grid : {sharpLines : true, + borderVisible : false, + verticalSections : 4, + millisPerLine : 10000}, + labels : {disabled : false}, + timestampFormatter : SmoothieChart.timeFormatter, + millisPerPixel : 100 //tooltip : true + }; + + canvas_node.id = "canvas_" + this.info.id; + canvas_node.width = 400; + canvas_node.height = 154; + + widget_content.appendChild(canvas_node); + + if (this.info.max_val !== "auto") { + s_settings.maxValue = this.info.max_val; + } + + if (this.info.min_val !== "auto") { + s_settings.minValue = this.info.min_val; + } + + this.smoothie = new SmoothieChart(s_settings); + this.lines = []; + this.smoothie.streamTo(document.getElementById(canvas_node.id), + this.info.refresh * 1000); + } + }; + + /** + * Fill table headers + * + * @param {object} table Parent table + * + * @returns {object} + */ + Widget.prototype.fill_table_headers = function (table) { + var col_set = []; + + if (this.data !== null) { + var tr = document.createElement("tr"); + for (var i = 0, l = this.data.length; i < l; i++) { + for (var key in this.data[i]) { + if (this.data[i].hasOwnProperty(key) && col_set.indexOf(key) === -1) { + col_set.push(key); + + var th = document.createElement("th"); + + th.appendChild(document.createTextNode(key)); + tr.appendChild(th); + } + } + } + table.appendChild(tr); + } + + return col_set; + }; + + /** + * Return HTML table filled with data + * + * @returns {object} + */ + Widget.prototype.html_table = function () { + var table = document.createElement("table"), + tr; + + if (this.data !== null) { + if (this.info.type === this.parent_app.OT_TABLE_V) { + var even_line = false; + for (var key in this.data[0]) { + if (this.data[0].hasOwnProperty(key)) { + var td_key = document.createElement("td"), + td_val = document.createElement("td"); + + tr = document.createElement("tr"); + + td_key.appendChild(document.createTextNode(key)); + td_key.classList.add("table_header"); + if (this.info.raw_num) { + td_val.appendChild(document.createTextNode(this.data[0][key] || "")); + } else { + td_val.appendChild( + document.createTextNode( + numFormatted(this.data[0][key], "'") || "" + ) + ); + } + + tr.appendChild(td_key); + tr.appendChild(td_val); + + if (even_line) { + tr.classList.add("even_line"); + } + + even_line = !even_line; + table.appendChild(tr); + } + } + } else { + var columns = this.fill_table_headers(table); + + for (var i = 0, max_i = this.data.length; i < max_i; ++i) { + tr = document.createElement("tr"); + + for (var j = 0, max_j = columns.length; j < max_j ; ++j) { + var td = document.createElement("td"); + + if (this.info.raw_num) { + td.appendChild( + document.createTextNode( + this.data[i][columns[j]] || "" + ) + ); + } else { + td.appendChild( + document.createTextNode( + numFormatted(this.data[i][columns[j]], "'") || "" + ) + ); + } + tr.appendChild(td); + } + + if (i % 2 !== 0) { + tr.classList.add("even_line"); + } + table.appendChild(tr); + } + } + } + return table; + }; + + /** + * Update canvas widget data + * + * @param {object} app_instance Application object instance + * @param {numeric} index Widget index + * + * @returns {undefined} + */ + Widget.prototype.update_canvas_data = function (app_instance, index) { + var w_instance = app_instance.widgets[index], + colors = [ + ["rgb(0, 255, 0)", "rgba(0, 255, 0, 0.4)"], // green + ["rgb(255, 0, 0)", "rgba(255, 0, 0, 0.4)"], // red + ["rgb(255, 255, 0)", "rgba(255, 255, 0, 0.4)"], // yellow + ["rgb(0, 255, 255)", "rgba(0, 255, 255, 0.4)"], + ["rgb(0, 0, 255)", "rgba(0, 0, 255, 0.4)"], // blue + ["rgb(255, 255, 255)", "rgba(255, 255, 255, 0.4)"], + ["rgb(128, 255, 255)", "rgba(128, 255, 255, 0.4)"], + ["rgb(255, 128, 255)", "rgba(255, 128, 255, 0.4)"], + ["rgb(255, 255, 128)", "rgba(255, 255, 128, 0.4)"], + ["rgb(128, 128, 255)", "rgba(128, 128, 255, 0.4)"], + ["rgb(255, 128, 128)", "rgba(255, 128, 128, 0.4)"], + ["rgb(128, 255, 128)", "rgba(128, 255, 128, 0.4)"], + ["rgb(0, 128, 255)", "rgba(0, 128, 255, 0.4)"], + ["rgb(255, 0, 128)", "rgba(255, 0, 128, 0.4)"], + ["rgb(128, 255, 0)", "rgba(128, 255, 0, 0.4)"], + ["rgb(0, 0, 128)", "rgba(0, 0, 128, 0.4)"] + ]; + + app_instance.get_data(w_instance.info.json_uri) + .then(function(result) { + var json_data = JSON.parse(result), + i = 0; + + if (document.getElementById(w_instance.info.id) === null) { + return; + } + + var content_container = document.getElementById(w_instance.info.id).children[1], + div_legend; + + for (var key in json_data) { + if (json_data.hasOwnProperty(key)) { + if (w_instance.lines.length < i + 1) { + div_legend = document.createElement("div"); + div_legend.id = w_instance.info.id + "_" + key; + div_legend.style.color = colors[i][0]; + div_legend.classList.add("canvas_legend"); + div_legend.appendChild(document.createTextNode (key)); + + content_container.appendChild(div_legend); + + w_instance.lines.push(new TimeSeries()); + + w_instance.smoothie.addTimeSeries( + w_instance.lines[i], + {strokeStyle : colors[i][0], + fillStyle : colors[i][1], + lineWidth : 3} + ); + } else { + div_legend = document.getElementById( + w_instance.info.id + "_" + key + ); + + if (div_legend === null) { + return; + } + } + div_legend.textContent = key + ": " + + json_data[key] + + w_instance.info.measure; + div_legend.classList.remove("slightly_visible"); + + w_instance.lines[i].append(new Date().getTime(), + Math.floor(json_data[key])); + document.getElementById(w_instance.info.id).classList.remove("slightly_visible"); + i++; + } + } + }) + .catch(function(result) { + console_log("update_canvas_data error: " + result); + }); + }; + + /** + * Draw table widget + * + * @param {object} app_instance Application object instance + * @param {numeric} index Widget index + * + * @returns {undefined} + */ + Widget.prototype.update_table_data = function (app_instance, index) { + var w_instance = app_instance.widgets[index]; + + app_instance.get_data(w_instance.info.json_uri) + .then(function(result) { + var json_data = JSON.parse(result); + + if (document.getElementById(w_instance.info.id) !== null) { + w_instance.data = json_data; + w_instance.draw(); + document.getElementById(w_instance.info.id).classList.remove("slightly_visible"); + } + }) + .catch(function(result) { + console_log("update_table_data error: " + result); + }); + }; + + document.addEventListener("DOMContentLoaded", function () { + /** + * Initialize application object + */ + var app = new Application(); + }); + +}()); diff --git a/js/smoothie.js b/js/smoothie.js new file mode 100644 index 0000000..2d85f63 --- /dev/null +++ b/js/smoothie.js @@ -0,0 +1,1047 @@ +// MIT License: +// +// Copyright (c) 2010-2013, Joe Walnes +// 2013-2018, Drew Noakes +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * Smoothie Charts - http://smoothiecharts.org/ + * (c) 2010-2013, Joe Walnes + * 2013-2018, Drew Noakes + * + * v1.0: Main charting library, by Joe Walnes + * v1.1: Auto scaling of axis, by Neil Dunn + * v1.2: fps (frames per second) option, by Mathias Petterson + * v1.3: Fix for divide by zero, by Paul Nikitochkin + * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds + * v1.5: Set default frames per second to 50... smoother. + * .start(), .stop() methods for conserving CPU, by Dmitry Vyal + * options.interpolation = 'bezier' or 'line', by Dmitry Vyal + * options.maxValue to fix scale, by Dmitry Vyal + * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla + * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin + * Smooth rescaling, by Kostas Michalopoulos + * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni + * v1.9: Display timestamps along the bottom, by Nick and Stev-io + * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) + * Refactored by Krishna Narni, to support timestamp formatting function + * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh + * v1.11: options.grid.sharpLines option added, by @drewnoakes + * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes + * v1.12: Support for horizontalLines added, by @drewnoakes + * Support for yRangeFunction callback added, by @drewnoakes + * v1.13: Fixed typo (#32), by @alnikitich + * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano + * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes + * v1.15: Support for npm package (#18), by @dominictarr + * Fixed broken removeTimeSeries function (#24) by @davidgaleano + * Minor performance and tidying, by @drewnoakes + * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes + * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) + * Documentation and some local variable renaming for clarity, by @drewnoakes + * v1.17: Allow control over font size (#10), by @drewnoakes + * Timestamp text won't overlap, by @drewnoakes + * v1.18: Allow control of max/min label precision, by @drewnoakes + * Added 'borderVisible' chart option, by @drewnoakes + * Allow drawing series with fill but no stroke (line), by @drewnoakes + * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai + * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes + * v1.21: Add 'step' interpolation mode, by @drewnoakes + * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic + * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes + * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf + * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92 + * Draw time labels on top of series, by @comolosabia + * Add TimeSeries.clear function, by @drewnoakes + * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic + * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush + * v1.28: Add 'minValueScale' option, by @megawac + * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn + * v1.29: Support responsive sizing, by @drewnoakes + * v1.29.1: Include types in package, and make property optional, by @TrentHouliston + * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime + * v1.31: Support tooltips, by @Sly1024 and @drewnoakes + * v1.32: Support frame rate limit, by @dpuyosa + * v1.33: Use Date static method instead of instance, by @nnnoel + * Fix bug with tooltips when multiple charts on a page, by @jpmbiz70 + * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91) + * Add nonRealtimeData option, by @annazhelt (#92, #93) + * Add showIntermediateLabels option, by @annazhelt (#94) + * Add displayDataFromPercentile option, by @annazhelt (#95) + * Fix bug when hiding tooltip element, by @ralphwetzel (#96) + * Support intermediate y-axis labels, by @beikeland (#99) + * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101) + */ + +;(function(exports) { + + // Date.now polyfill + Date.now = Date.now || function() { return new Date().getTime(); }; + + var Util = { + extend: function() { + arguments[0] = arguments[0] || {}; + for (var i = 1; i < arguments.length; i++) + { + for (var key in arguments[i]) + { + if (arguments[i].hasOwnProperty(key)) + { + if (typeof(arguments[i][key]) === 'object') { + if (arguments[i][key] instanceof Array) { + arguments[0][key] = arguments[i][key]; + } else { + arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); + } + } else { + arguments[0][key] = arguments[i][key]; + } + } + } + } + return arguments[0]; + }, + binarySearch: function(data, value) { + var low = 0, + high = data.length; + while (low < high) { + var mid = (low + high) >> 1; + if (value < data[mid][0]) + high = mid; + else + low = mid + 1; + } + return low; + } + }; + + /** + * Initialises a new TimeSeries with optional data options. + * + * Options are of the form (defaults shown): + * + *
+   * {
+   *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
+   *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
+   * }
+   * 
+ * + * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. + * + * @constructor + */ + function TimeSeries(options) { + this.options = Util.extend({}, TimeSeries.defaultOptions, options); + this.disabled = false; + this.clear(); + } + + TimeSeries.defaultOptions = { + resetBoundsInterval: 3000, + resetBounds: true + }; + + /** + * Clears all data and state from this TimeSeries object. + */ + TimeSeries.prototype.clear = function() { + this.data = []; + this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. + this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. + }; + + /** + * Recalculate the min/max values for this TimeSeries object. + * + * This causes the graph to scale itself in the y-axis. + */ + TimeSeries.prototype.resetBounds = function() { + if (this.data.length) { + // Walk through all data points, finding the min/max value + this.maxValue = this.data[0][1]; + this.minValue = this.data[0][1]; + for (var i = 1; i < this.data.length; i++) { + var value = this.data[i][1]; + if (value > this.maxValue) { + this.maxValue = value; + } + if (value < this.minValue) { + this.minValue = value; + } + } + } else { + // No data exists, so set min/max to NaN + this.maxValue = Number.NaN; + this.minValue = Number.NaN; + } + }; + + /** + * Adds a new data point to the TimeSeries, preserving chronological order. + * + * @param timestamp the position, in time, of this data point + * @param value the value of this data point + * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls + * whether it is replaced, or the values summed (defaults to false.) + */ + TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { + // Rewind until we hit an older timestamp + var i = this.data.length - 1; + while (i >= 0 && this.data[i][0] > timestamp) { + i--; + } + + if (i === -1) { + // This new item is the oldest data + this.data.splice(0, 0, [timestamp, value]); + } else if (this.data.length > 0 && this.data[i][0] === timestamp) { + // Update existing values in the array + if (sumRepeatedTimeStampValues) { + // Sum this value into the existing 'bucket' + this.data[i][1] += value; + value = this.data[i][1]; + } else { + // Replace the previous value + this.data[i][1] = value; + } + } else if (i < this.data.length - 1) { + // Splice into the correct position to keep timestamps in order + this.data.splice(i + 1, 0, [timestamp, value]); + } else { + // Add to the end of the array + this.data.push([timestamp, value]); + } + + this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); + this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); + }; + + TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { + // We must always keep one expired data point as we need this to draw the + // line that comes into the chart from the left, but any points prior to that can be removed. + var removeCount = 0; + while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { + removeCount++; + } + if (removeCount !== 0) { + this.data.splice(0, removeCount); + } + }; + + /** + * Initialises a new SmoothieChart. + * + * Options are optional, and should be of the form below. Just specify the values you + * need and the rest will be given sensible defaults as shown: + * + *
+   * {
+   *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
+   *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
+   *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
+   *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
+   *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
+   *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
+   *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
+   *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
+   *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
+   *     return parseFloat(min).toFixed(precision);
+   *   },
+   *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
+   *     return parseFloat(max).toFixed(precision);
+   *   },
+   *   yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels
+   *     return parseFloat(intermediate).toFixed(precision);
+   *   },
+   *   maxDataSetLength: 2,
+   *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
+   *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
+   *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
+   *   scrollBackwards: false,                   // reverse the scroll direction of the chart
+   *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
+   *   grid:
+   *   {
+   *     fillStyle: '#000000',                   // the background colour of the chart
+   *     lineWidth: 1,                           // the pixel width of grid lines
+   *     strokeStyle: '#777777',                 // colour of grid lines
+   *     millisPerLine: 1000,                    // distance between vertical grid lines
+   *     sharpLines: false,                      // controls whether grid lines are 1px sharp, or softened
+   *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
+   *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
+   *   },
+   *   labels
+   *   {
+   *     disabled: false,                        // enables/disables labels showing the min/max values
+   *     fillStyle: '#ffffff',                   // colour for text of labels,
+   *     fontSize: 15,
+   *     fontFamily: 'sans-serif',
+   *     precision: 2,
+   *     showIntermediateLabels: false,          // shows intermediate labels between min and max values along y axis
+   *     intermediateLabelSameAxis: true,
+   *   },
+   *   tooltip: false                            // show tooltip when mouse is over the chart
+   *   tooltipLine: {                            // properties for a vertical line at the cursor position
+   *     lineWidth: 1,
+   *     strokeStyle: '#BBBBBB'
+   *   },
+   *   tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
+   *   nonRealtimeData: false,                   // use time of latest data as current time
+   *   displayDataFromPercentile: 1,             // display not latest data, but data from the given percentile
+   *                                             // useful when trying to see old data saved by setting a high value for maxDataSetLength
+   *                                             // should be a value between 0 and 1
+   *   responsive: false,                        // whether the chart should adapt to the size of the canvas
+   *   limitFPS: 0                               // maximum frame rate the chart will render at, in FPS (zero means no limit)
+   * }
+   * 
+ * + * @constructor + */ + function SmoothieChart(options) { + this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); + this.seriesSet = []; + this.currentValueRange = 1; + this.currentVisMinValue = 0; + this.lastRenderTimeMillis = 0; + this.lastChartTimestamp = 0; + + this.mousemove = this.mousemove.bind(this); + this.mouseout = this.mouseout.bind(this); + } + + /** Formats the HTML string content of the tooltip. */ + SmoothieChart.tooltipFormatter = function (timestamp, data) { + var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter, + lines = [timestampFormatter(new Date(timestamp))]; + + for (var i = 0; i < data.length; ++i) { + lines.push('' + + this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + ''); + } + + return lines.join('
'); + }; + + SmoothieChart.defaultChartOptions = { + millisPerPixel: 20, + enableDpiScaling: true, + yMinFormatter: function(min, precision) { + return parseFloat(min).toFixed(precision); + }, + yMaxFormatter: function(max, precision) { + return parseFloat(max).toFixed(precision); + }, + yIntermediateFormatter: function(intermediate, precision) { + return parseFloat(intermediate).toFixed(precision); + }, + maxValueScale: 1, + minValueScale: 1, + interpolation: 'bezier', + scaleSmoothing: 0.125, + maxDataSetLength: 2, + scrollBackwards: false, + displayDataFromPercentile: 1, + grid: { + fillStyle: '#000000', + strokeStyle: '#777777', + lineWidth: 1, + sharpLines: false, + millisPerLine: 1000, + verticalSections: 2, + borderVisible: true + }, + labels: { + fillStyle: '#ffffff', + disabled: false, + fontSize: 10, + fontFamily: 'monospace', + precision: 2, + showIntermediateLabels: false, + intermediateLabelSameAxis: true, + }, + horizontalLines: [], + tooltip: false, + tooltipLine: { + lineWidth: 1, + strokeStyle: '#BBBBBB' + }, + tooltipFormatter: SmoothieChart.tooltipFormatter, + nonRealtimeData: false, + responsive: false, + limitFPS: 0 + }; + + // Based on http://inspirit.github.com/jsfeat/js/compatibility.js + SmoothieChart.AnimateCompatibility = (function() { + var requestAnimationFrame = function(callback, element) { + var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(function() { + callback(Date.now()); + }, 16); + }; + return requestAnimationFrame.call(window, callback, element); + }, + cancelAnimationFrame = function(id) { + var cancelAnimationFrame = + window.cancelAnimationFrame || + function(id) { + clearTimeout(id); + }; + return cancelAnimationFrame.call(window, id); + }; + + return { + requestAnimationFrame: requestAnimationFrame, + cancelAnimationFrame: cancelAnimationFrame + }; + })(); + + SmoothieChart.defaultSeriesPresentationOptions = { + lineWidth: 1, + strokeStyle: '#ffffff' + }; + + /** + * Adds a TimeSeries to this chart, with optional presentation options. + * + * Presentation options should be of the form (defaults shown): + * + *
+   * {
+   *   lineWidth: 1,
+   *   strokeStyle: '#ffffff',
+   *   fillStyle: undefined
+   * }
+   * 
+ */ + SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { + this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); + if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { + timeSeries.resetBoundsTimerId = setInterval( + function() { + timeSeries.resetBounds(); + }, + timeSeries.options.resetBoundsInterval + ); + } + }; + + /** + * Removes the specified TimeSeries from the chart. + */ + SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + this.seriesSet.splice(i, 1); + break; + } + } + // If a timer was operating for that timeseries, remove it + if (timeSeries.resetBoundsTimerId) { + // Stop resetting the bounds, if we were + clearInterval(timeSeries.resetBoundsTimerId); + } + }; + + /** + * Gets render options for the specified TimeSeries. + * + * As you may use a single TimeSeries in multiple charts with different formatting in each usage, + * these settings are stored in the chart. + */ + SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + return this.seriesSet[i].options; + } + } + }; + + /** + * Brings the specified TimeSeries to the top of the chart. It will be rendered last. + */ + SmoothieChart.prototype.bringToFront = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + var set = this.seriesSet.splice(i, 1); + this.seriesSet.push(set[0]); + break; + } + } + }; + + /** + * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. + * + * @param canvas the target canvas element + * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series + * from appearing on screen, with new values flashing into view, at the expense of some latency. + */ + SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { + this.canvas = canvas; + this.delay = delayMillis; + this.start(); + }; + + SmoothieChart.prototype.getTooltipEl = function () { + // Create the tool tip element lazily + if (!this.tooltipEl) { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'smoothie-chart-tooltip'; + this.tooltipEl.style.position = 'absolute'; + this.tooltipEl.style.display = 'none'; + document.body.appendChild(this.tooltipEl); + } + return this.tooltipEl; + }; + + SmoothieChart.prototype.updateTooltip = function () { + var el = this.getTooltipEl(); + + if (!this.mouseover || !this.options.tooltip) { + el.style.display = 'none'; + return; + } + + var time = this.lastChartTimestamp; + + // x pixel to time + var t = this.options.scrollBackwards + ? time - this.mouseX * this.options.millisPerPixel + : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel; + + var data = []; + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + // find datapoint closest to time 't' + var closeIdx = Util.binarySearch(timeSeries.data, t); + if (closeIdx > 0 && closeIdx < timeSeries.data.length) { + data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] }); + } + } + + if (data.length) { + el.innerHTML = this.options.tooltipFormatter.call(this, t, data); + el.style.display = 'block'; + } else { + el.style.display = 'none'; + } + }; + + SmoothieChart.prototype.mousemove = function (evt) { + this.mouseover = true; + this.mouseX = evt.offsetX; + this.mouseY = evt.offsetY; + this.mousePageX = evt.pageX; + this.mousePageY = evt.pageY; + + var el = this.getTooltipEl(); + el.style.top = Math.round(this.mousePageY) + 'px'; + el.style.left = Math.round(this.mousePageX) + 'px'; + this.updateTooltip(); + }; + + SmoothieChart.prototype.mouseout = function () { + this.mouseover = false; + this.mouseX = this.mouseY = -1; + if (this.tooltipEl) + this.tooltipEl.style.display = 'none'; + }; + + /** + * Make sure the canvas has the optimal resolution for the device's pixel ratio. + */ + SmoothieChart.prototype.resize = function () { + var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio, + width, height; + if (this.options.responsive) { + // Newer behaviour: Use the canvas's size in the layout, and set the internal + // resolution according to that size and the device pixel ratio (eg: high DPI) + width = this.canvas.offsetWidth; + height = this.canvas.offsetHeight; + + if (width !== this.lastWidth) { + this.lastWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.getContext('2d').scale(dpr, dpr); + } + if (height !== this.lastHeight) { + this.lastHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.getContext('2d').scale(dpr, dpr); + } + } else if (dpr !== 1) { + // Older behaviour: use the canvas's inner dimensions and scale the element's size + // according to that size and the device pixel ratio (eg: high DPI) + width = parseInt(this.canvas.getAttribute('width')); + height = parseInt(this.canvas.getAttribute('height')); + + if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) { + this.originalWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.style.width = width + 'px'; + this.canvas.getContext('2d').scale(dpr, dpr); + } + + if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) { + this.originalHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.style.height = height + 'px'; + this.canvas.getContext('2d').scale(dpr, dpr); + } + } + }; + + /** + * Starts the animation of this chart. + */ + SmoothieChart.prototype.start = function() { + if (this.frame) { + // We're already running, so just return + return; + } + + this.canvas.addEventListener('mousemove', this.mousemove); + this.canvas.addEventListener('mouseout', this.mouseout); + + // Renders a frame, and queues the next frame for later rendering + var animate = function() { + this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { + if(this.options.nonRealtimeData){ + var dateZero = new Date(0); + // find the data point with the latest timestamp + var maxTimeStamp = this.seriesSet.reduce(function(max, series){ + var dataSet = series.timeSeries.data; + var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1; + indexToCheck = indexToCheck >= 0 ? indexToCheck : 0; + indexToCheck = indexToCheck <= dataSet.length -1 ? indexToCheck : dataSet.length -1; + if(dataSet && dataSet.length > 0) + { + // timestamp corresponds to element 0 of the data point + var lastDataTimeStamp = dataSet[indexToCheck][0]; + max = max > lastDataTimeStamp ? max : lastDataTimeStamp; + } + return max; + }.bind(this), dateZero); + // use the max timestamp as current time + this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null); + } else { + this.render(); + } + animate(); + }.bind(this)); + }.bind(this); + + animate(); + }; + + /** + * Stops the animation of this chart. + */ + SmoothieChart.prototype.stop = function() { + if (this.frame) { + SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); + delete this.frame; + this.canvas.removeEventListener('mousemove', this.mousemove); + this.canvas.removeEventListener('mouseout', this.mouseout); + } + }; + + SmoothieChart.prototype.updateValueRange = function() { + // Calculate the current scale of the chart, from all time series. + var chartOptions = this.options, + chartMaxValue = Number.NaN, + chartMinValue = Number.NaN; + + for (var d = 0; d < this.seriesSet.length; d++) { + // TODO(ndunn): We could calculate / track these values as they stream in. + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + if (!isNaN(timeSeries.maxValue)) { + chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; + } + + if (!isNaN(timeSeries.minValue)) { + chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; + } + } + + // Scale the chartMaxValue to add padding at the top if required + if (chartOptions.maxValue != null) { + chartMaxValue = chartOptions.maxValue; + } else { + chartMaxValue *= chartOptions.maxValueScale; + } + + // Set the minimum if we've specified one + if (chartOptions.minValue != null) { + chartMinValue = chartOptions.minValue; + } else { + chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); + } + + // If a custom range function is set, call it + if (this.options.yRangeFunction) { + var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); + chartMinValue = range.min; + chartMaxValue = range.max; + } + + if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { + var targetValueRange = chartMaxValue - chartMinValue; + var valueRangeDiff = (targetValueRange - this.currentValueRange); + var minValueDiff = (chartMinValue - this.currentVisMinValue); + this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; + this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; + this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; + } + + this.valueRange = { min: chartMinValue, max: chartMaxValue }; + }; + + SmoothieChart.prototype.render = function(canvas, time) { + var nowMillis = Date.now(); + + // Respect any frame rate limit. + if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS)) + return; + + if (!this.isAnimatingScale) { + // We're not animating. We can use the last render time and the scroll speed to work out whether + // we actually need to paint anything yet. If not, we can return immediately. + + // Render at least every 1/6th of a second. The canvas may be resized, which there is + // no reliable way to detect. + var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel); + + if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { + return; + } + } + + this.resize(); + this.updateTooltip(); + + this.lastRenderTimeMillis = nowMillis; + + canvas = canvas || this.canvas; + time = time || nowMillis - (this.delay || 0); + + // Round time down to pixel granularity, so motion appears smoother. + time -= time % this.options.millisPerPixel; + + this.lastChartTimestamp = time; + + var context = canvas.getContext('2d'), + chartOptions = this.options, + dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, + // Calculate the threshold time for the oldest data points. + oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), + valueToYPixel = function(value) { + var offset = value - this.currentVisMinValue; + return this.currentValueRange === 0 + ? dimensions.height + : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); + }.bind(this), + timeToXPixel = function(t) { + if(chartOptions.scrollBackwards) { + return Math.round((time - t) / chartOptions.millisPerPixel); + } + return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); + }; + + this.updateValueRange(); + + context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; + + // Save the state of the canvas context, any transformations applied in this method + // will get removed from the stack at the end of this method when .restore() is called. + context.save(); + + // Move the origin. + context.translate(dimensions.left, dimensions.top); + + // Create a clipped rectangle - anything we draw will be constrained to this rectangle. + // This prevents the occasional pixels from curves near the edges overrunning and creating + // screen cheese (that phrase should need no explanation). + context.beginPath(); + context.rect(0, 0, dimensions.width, dimensions.height); + context.clip(); + + // Clear the working area. + context.save(); + context.fillStyle = chartOptions.grid.fillStyle; + context.clearRect(0, 0, dimensions.width, dimensions.height); + context.fillRect(0, 0, dimensions.width, dimensions.height); + context.restore(); + + // Grid lines... + context.save(); + context.lineWidth = chartOptions.grid.lineWidth; + context.strokeStyle = chartOptions.grid.strokeStyle; + // Vertical (time) dividers. + if (chartOptions.grid.millisPerLine > 0) { + context.beginPath(); + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPixel(t); + if (chartOptions.grid.sharpLines) { + gx -= 0.5; + } + context.moveTo(gx, 0); + context.lineTo(gx, dimensions.height); + } + context.stroke(); + context.closePath(); + } + + // Horizontal (value) dividers. + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); + if (chartOptions.grid.sharpLines) { + gy -= 0.5; + } + context.beginPath(); + context.moveTo(0, gy); + context.lineTo(dimensions.width, gy); + context.stroke(); + context.closePath(); + } + // Bounding rectangle. + if (chartOptions.grid.borderVisible) { + context.beginPath(); + context.strokeRect(0, 0, dimensions.width, dimensions.height); + context.closePath(); + } + context.restore(); + + // Draw any horizontal lines... + if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { + for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { + var line = chartOptions.horizontalLines[hl], + hly = Math.round(valueToYPixel(line.value)) - 0.5; + context.strokeStyle = line.color || '#ffffff'; + context.lineWidth = line.lineWidth || 1; + context.beginPath(); + context.moveTo(0, hly); + context.lineTo(dimensions.width, hly); + context.stroke(); + context.closePath(); + } + } + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + context.save(); + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + var dataSet = timeSeries.data, + seriesOptions = this.seriesSet[d].options; + + // Delete old data that's moved off the left of the chart. + timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); + + // Set style for this dataSet. + context.lineWidth = seriesOptions.lineWidth; + context.strokeStyle = seriesOptions.strokeStyle; + // Draw the line... + context.beginPath(); + // Retain lastX, lastY for calculating the control points of bezier curves. + var firstX = 0, lastX = 0, lastY = 0; + for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { + var x = timeToXPixel(dataSet[i][0]), + y = valueToYPixel(dataSet[i][1]); + + if (i === 0) { + firstX = x; + context.moveTo(x, y); + } else { + switch (chartOptions.interpolation) { + case "linear": + case "line": { + context.lineTo(x,y); + break; + } + case "bezier": + default: { + // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves + // + // Assuming A was the last point in the line plotted and B is the new point, + // we draw a curve with control points P and Q as below. + // + // A---P + // | + // | + // | + // Q---B + // + // Importantly, A and P are at the same y coordinate, as are B and Q. This is + // so adjacent curves appear to flow as one. + // + context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop + Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) + Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) + x, y); // endPoint (B) + break; + } + case "step": { + context.lineTo(x,lastY); + context.lineTo(x,y); + break; + } + } + } + + lastX = x; lastY = y; + } + + if (dataSet.length > 1) { + if (seriesOptions.fillStyle) { + // Close up the fill region. + context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); + context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); + context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); + context.fillStyle = seriesOptions.fillStyle; + context.fill(); + } + + if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { + context.stroke(); + } + context.closePath(); + } + context.restore(); + } + + if (chartOptions.tooltip && this.mouseX >= 0) { + // Draw vertical bar to show tooltip position + context.lineWidth = chartOptions.tooltipLine.lineWidth; + context.strokeStyle = chartOptions.tooltipLine.strokeStyle; + context.beginPath(); + context.moveTo(this.mouseX, 0); + context.lineTo(this.mouseX, dimensions.height); + context.closePath(); + context.stroke(); + this.updateTooltip(); + } + + // Draw the axis values on the chart. + if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { + var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision), + minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision), + maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2, + minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2; + context.fillStyle = chartOptions.labels.fillStyle; + context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize); + context.fillText(minValueString, minLabelPos, dimensions.height - 2); + } + + // Display intermediate y axis labels along y-axis to the left of the chart + if ( chartOptions.labels.showIntermediateLabels + && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max) + && chartOptions.grid.verticalSections > 0) { + // show a label above every vertical section divider + var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections; + var stepPixels = dimensions.height / chartOptions.grid.verticalSections; + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = dimensions.height - Math.round(v * stepPixels); + if (chartOptions.grid.sharpLines) { + gy -= 0.5; + } + var yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + (v * step), chartOptions.labels.precision); + //left of right axis? + intermediateLabelPos = + chartOptions.labels.intermediateLabelSameAxis + ? (chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(yValue).width - 2) + : (chartOptions.scrollBackwards ? dimensions.width - context.measureText(yValue).width - 2 : 0); + + context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth); + } + } + + // Display timestamps along x-axis at the bottom of the chart. + if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { + var textUntilX = chartOptions.scrollBackwards + ? context.measureText(minValueString).width + : dimensions.width - context.measureText(minValueString).width + 4; + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPixel(t); + // Only draw the timestamp if it won't overlap with the previously drawn one. + if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { + // Formats the timestamp based on user specified formatting function + // SmoothieChart.timeFormatter function above is one such formatting option + var tx = new Date(t), + ts = chartOptions.timestampFormatter(tx), + tsWidth = context.measureText(ts).width; + + textUntilX = chartOptions.scrollBackwards + ? gx + tsWidth + 2 + : gx - tsWidth - 2; + + context.fillStyle = chartOptions.labels.fillStyle; + if(chartOptions.scrollBackwards) { + context.fillText(ts, gx, dimensions.height - 2); + } else { + context.fillText(ts, gx - tsWidth, dimensions.height - 2); + } + } + } + } + + context.restore(); // See .save() above. + }; + + // Sample timestamp formatting function + SmoothieChart.timeFormatter = function(date) { + function pad2(number) { return (number < 10 ? '0' : '') + number } + return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); + }; + + exports.TimeSeries = TimeSeries; + exports.SmoothieChart = SmoothieChart; + +})(typeof exports === 'undefined' ? this : exports); diff --git a/system/action/index.php b/system/action/index.php new file mode 100644 index 0000000..557c474 --- /dev/null +++ b/system/action/index.php @@ -0,0 +1,62 @@ +translate('reboot_in_progress') . '
'; + echo shell_exec('sudo shutdown -r'); + } else { + echo $lang->translate('reboot_prohibited') . '
'; + } +} else if ($action === 'shutdown') { + if (SHUTDOWN_ENABLED) { + echo $lang->translate('shutdown_in_progress') . '
'; + echo shell_exec('sudo halt -p'); + } else { + echo $lang->translate('shutdown_prohibited') . '
'; + } +} else if ($action === 'setup') { + $def_lang = filter_input(INPUT_GET, 'default_language', FILTER_SANITIZE_STRING); + $dim_on_create = filter_input(INPUT_GET, 'dim_on_create', FILTER_SANITIZE_NUMBER_INT) == 1; + $chk_f_rights = filter_input(INPUT_GET, 'chk_files_rights', FILTER_SANITIZE_NUMBER_INT) == 1; + $max_hdd_temp = filter_input(INPUT_GET, 'max_hdd_temp', FILTER_SANITIZE_NUMBER_INT); + $chk_hdd_temp_int = filter_input(INPUT_GET, 'chk_temp_interval', FILTER_SANITIZE_NUMBER_INT); + $chk_smart_int = filter_input(INPUT_GET, 'chk_smart_interval', FILTER_SANITIZE_NUMBER_INT); + $chk_users_online_int = filter_input(INPUT_GET, 'chk_users_online_interval', FILTER_SANITIZE_NUMBER_INT); + + $user_cfg->set_value('lang', $def_lang); + $user_cfg->set_value('dim_on_create', $dim_on_create); + $user_cfg->set_value('check_files_rights', $chk_f_rights); + $user_cfg->set_value('max_hdd_temp', $max_hdd_temp); + $user_cfg->set_value('check_hdd_temp_interval', $chk_hdd_temp_int); + $user_cfg->set_value('check_smart_interval', $chk_smart_int); + $user_cfg->set_value('check_users_online_interval', $chk_users_online_int); + + $uc_saved = $user_cfg->save(); + + /* Apps list */ + $enabled_apps = array(); + foreach ($_GET["app_caption"] as $item => $val) { + if (isset($_GET["app_enabled"][$item]) && (bool)$_GET["app_enabled"][$item]) { + $app_cap = filter_var($_GET["app_caption"][$item], FILTER_SANITIZE_STRING); + $app_name = filter_var($_GET["app_name"][$item], FILTER_SANITIZE_STRING); + $enabled_apps[$app_cap] = $app_name; + } + } + $app = new \CAI\CAICP\Applications(CP_ROOT_REL); + $app->set($enabled_apps); + $apps_saved = $app->save(); + + if ($uc_saved && $apps_saved) { + echo 'true'; + } else { + echo 'false'; + } +} diff --git a/system/apps/apps.json b/system/apps/apps.json new file mode 100644 index 0000000..af0830d --- /dev/null +++ b/system/apps/apps.json @@ -0,0 +1,11 @@ +{ + "Plex" : "plex", + "Transmission" : "transmission", + "Resilio sync" : "rslsync", + "NextCloud" : "nextcloud", + "OwnCloud" : "owncloud", + "Files downloader" : "cai_downloader", + "OpenFire" : "openfire", + "phpMyAdmin" : "phpmyadmin", + "phpPgAdmin" : "phppgadmin" +} diff --git a/system/apps/cai_downloader/app.json b/system/apps/cai_downloader/app.json new file mode 100644 index 0000000..d39eb2c --- /dev/null +++ b/system/apps/cai_downloader/app.json @@ -0,0 +1,16 @@ +{ + "app_name" : "app_cai_downloader", + "caption" : "Files downloader", + "version" : "1.0.20170705", + "author" : "Alexander I. Chebykin", + "http_uri" : "system/apps/cai_downloader/", + "use_http" : true, + "https_uri" : "system/apps/cai_downloader/", + "use_https" : true, + "frame_support" : true, + "require_php" : true, + "require" : [ + {"data" : "php"}, + {"data" : "curl"} + ] +} diff --git a/system/apps/cai_downloader/css/downloader.css b/system/apps/cai_downloader/css/downloader.css new file mode 100644 index 0000000..5943c9f --- /dev/null +++ b/system/apps/cai_downloader/css/downloader.css @@ -0,0 +1,32 @@ +.full_width {width: 100%} +.hidden {opacity: 0;} +.td_label {width: 100px;} +.del_rec { + position: absolute; + right: 15px; + cursor: pointer; +} + +#in_submit {margin: 10px auto;} +#div_download, +#div_log, +.div_log_rec {transition: all 0.5s ease-out 0.1s;} + +.div_log_rec span {margin-right: 20px;} + +fieldset { + border-width: 1px; + border-color: #ddd; +} + +hr { + margin: 5px 0; + padding: 0; + border: 0; + height: 1px; + background-image: -webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background: linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0, 0)); +} \ No newline at end of file diff --git a/system/apps/cai_downloader/exec/index.php b/system/apps/cai_downloader/exec/index.php new file mode 100644 index 0000000..26133a5 --- /dev/null +++ b/system/apps/cai_downloader/exec/index.php @@ -0,0 +1,69 @@ + + * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin + * @version : 1.0 + * @build date : 2017-07-17 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +require_once('../../../settings.php'); + +/* Places to store files by type */ +$ParentDir = array('.gif' => 'images/_animations/', + '.jpg' => 'images/', '.jpeg' => 'images/', '.png' => 'images/', + '.tif' => 'images/', '.tiff' => 'images/', + '.avi' => 'video/', '.flv' => 'video/', '.mkv' => 'video/', + '.mp4' => 'video/', '.mpeg' => 'video/', '.mpg' => 'video/', + '.exe' => 'soft/', '.msi' => 'soft/', + '.7z' => 'archives/', '.iso' => 'archives/', '.rar' => 'archives/', + '.tar' => 'archives/', '.zip' => 'archives/'); +/* Filter parameters */ +$Dir = str_replace(array('.', '/', '~', '\\', ' ', '%'), + '_', + filter_input(INPUT_POST, 'dir', FILTER_SANITIZE_STRING) + ); +$Overwrite = (int)filter_input(INPUT_POST, 'overwrite', FILTER_SANITIZE_NUMBER_INT); + +$URI = filter_input(INPUT_POST, 'uri', FILTER_SANITIZE_URL); + +$SubstrPos = 0; +$PDir = ''; +foreach ($ParentDir as $Key => $Val) { + if (mb_stripos($URI, $Key) !== false){ + $SPos = mb_stripos($URI, $Key); + if ($SubstrPos == 0) { + $SubstrPos = $SPos; + $PDir = $Val; + } else { + if ($SubstrPos < $Pos && $Pos > 0) { + $SubstrPos = $Pos; + $PDir = $Val; + } + } + } +} + +if ($Overwrite !== 1 && file_exists(str_replace('\ ', ' ', FILE_DOWNLOAD_DIR) . $PDir . $Dir . '/')) { + $i = 1; + while (file_exists(str_replace('\ ', ' ', FILE_DOWNLOAD_DIR) . $PDir . $Dir . '/' . $i)) { + $i++; + } + $Dir .= '/' . $i; +} + +echo exec(sprintf('bash -c "%s > /dev/null 2>&1 &"', + sprintf("./scripts/app.sh %s %s", + FILE_DOWNLOAD_DIR . $PDir . $Dir, + $URI) + ) + ); +echo '[langDownloadStarted]'; diff --git a/system/apps/cai_downloader/exec/scripts/app.sh b/system/apps/cai_downloader/exec/scripts/app.sh new file mode 100644 index 0000000..e6fcb10 --- /dev/null +++ b/system/apps/cai_downloader/exec/scripts/app.sh @@ -0,0 +1,3 @@ +#! /bin/bash +mkdir -p "$1" && cd "$1" && curl -L -S -O $2 >> download.log 2>&1 & +# mkdir -p "$1" && cd "$1" && curl -L -O $2 > /dev/null 2>&1 & \ No newline at end of file diff --git a/system/apps/cai_downloader/icon.png b/system/apps/cai_downloader/icon.png new file mode 100644 index 0000000..09bf9ad Binary files /dev/null and b/system/apps/cai_downloader/icon.png differ diff --git a/system/apps/cai_downloader/index.html b/system/apps/cai_downloader/index.html new file mode 100644 index 0000000..a5239e0 --- /dev/null +++ b/system/apps/cai_downloader/index.html @@ -0,0 +1,29 @@ + + + + Files downloader app for CAI CP + + + + + + + + + + diff --git a/system/apps/cai_downloader/js/downloader.js b/system/apps/cai_downloader/js/downloader.js new file mode 100644 index 0000000..678926f --- /dev/null +++ b/system/apps/cai_downloader/js/downloader.js @@ -0,0 +1,206 @@ +// MIT License: +// +// Copyright (c) 2016-2017, Alexander I. Chebykin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * CAI CP v.1 + * + * @module : Downloader app + * @author : Alexander I. Chebykin + * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin + * @version : 1.0 + * @build date : 2017-06-05 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +(function () { + 'use strict'; + + /** + * Return GET parameter from parent page + * + * @param {string} key Key name + * + * returns {string} + */ + function $_PARENT_GET(key) { +// var s = window.location.search; + var s = (window.location !== window.parent.location) + ? document.referrer + : document.location.href; + + s = s.match(new RegExp(key + '=([^&=]+)')); + + return s ? s[1] : false; + } + + /** + * Return GET parameter + * + * @param {string} key Key name + * + * returns {string} + */ + function $_GET(key) { +// var s = window.location.search; + var s = document.referrer; + + s = s.match(new RegExp(key + '=([^&=]+)')); + + return s ? s[1] : false; + } + + /** + * Download system constructor + * + * @returns {undefined} + */ + function DownloadSystem() { + this.locales = 'en ru'; + this.lang = {}; + } + + /** + * Download system initialization + * + * @param {string} lang Language code. Ex: en, ru etc. + * + * @returns {undefined} + */ + DownloadSystem.prototype.init = function (lang) { + var dl_instance = this, + json_file = 'locale/', + request = new XMLHttpRequest(); + + if (this.check_locale(lang)) { + json_file += lang + '.json'; + } else { + json_file += 'en.json'; + } + + if (document.getElementById('lang') !== null) { + document.getElementById('lang').value = lang; + } + + if ($_GET('overwrite') && (document.getElementById('overwrite') !== null)) { + document.getElementById('overwrite').checked = true; + } + + request.open('GET', json_file); + request.send(); + + request.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + dl_instance.lang = JSON.parse(this.responseText); + + document.getElementById('lbl_uri').textContent = dl_instance.lang.uri + ': '; + document.getElementById('lbl_dir').textContent = dl_instance.lang.dir + ': '; + document.getElementById('lbl_overwrite').textContent = dl_instance.lang.overwrite; + document.getElementById('lgn_log').textContent = dl_instance.lang.log; + document.getElementById('in_submit').value = dl_instance.lang.go; + document.getElementById('div_download').classList.toggle('hidden'); + document.getElementById('div_log').classList.toggle('hidden'); + } + } + }; + }; + + /** + * Check locale support + * + * @param {string} locale Locale code to check. Ex: en, ru etc. + * + * @returns {Boolean} + */ + DownloadSystem.prototype.check_locale = function (locale) { + return this.locales.indexOf(locale.toLowerCase()) !== -1; + }; + + document.addEventListener('DOMContentLoaded', function () { + /** + * Initialize download object + */ + var download = new DownloadSystem(); + + if ($_GET('lang')) { + download.init($_GET('lang')); + } else if ($_PARENT_GET('lang')) { + download.init($_PARENT_GET('lang')); + } else { + download.init('en'); + } + + document.getElementById('frm_submit').onsubmit = function () { + var xhr = new XMLHttpRequest(), + msg, + cur_date = new Date(); + + xhr.onload = function () { + var log_rec = document.createElement('div'), + log_span = document.createElement('span'), + img_del = document.createElement('img'), + hr = document.createElement('hr'); + + log_rec.classList.add('div_log_rec'); + log_rec.classList.add('hidden'); + + img_del.classList.add('del_rec'); + img_del.src = '../../../gfx/buttons/delete.png'; + img_del.title = download.lang.del_log_rec; + + img_del.addEventListener('click', function () { + this.parentNode.parentNode.removeChild(this.parentNode); + }); + + log_rec.appendChild(log_span); + log_rec.appendChild(img_del); + log_rec.appendChild(hr); + +// document.getElementById('div_log_recs').appendChild(log_rec); + document.getElementById('div_log_recs').insertBefore(log_rec, + document.getElementById('div_log_recs').childNodes[0]); + + if (xhr.responseText === '[langDownloadStarted]') { + msg = download.lang.download_started; + } else { + msg = xhr.responseText; + } + + log_span.appendChild(document.createTextNode( + cur_date.toLocaleString() + ': ' + + document.getElementById('dir').value + ' (' + + document.getElementById('uri').value + '): ' + + msg + ) + ); + + log_rec.classList.toggle('hidden'); + }; + + xhr.open(this.method, this.action, true); + xhr.send(new FormData(this)); + + return false; + }; + }); +}()); diff --git a/system/apps/cai_downloader/locale/en.json b/system/apps/cai_downloader/locale/en.json new file mode 100644 index 0000000..8f136de --- /dev/null +++ b/system/apps/cai_downloader/locale/en.json @@ -0,0 +1,9 @@ +{ + "uri" : "URI", + "dir" : "Directory", + "overwrite" : "Overwrite", + "go" : "Go!", + "log" : "Log", + "del_log_rec" : "Delete log record", + "download_started" : "Download started" +} diff --git a/system/apps/cai_downloader/locale/ru.json b/system/apps/cai_downloader/locale/ru.json new file mode 100644 index 0000000..7d37080 --- /dev/null +++ b/system/apps/cai_downloader/locale/ru.json @@ -0,0 +1,9 @@ +{ + "uri" : "URI", + "dir" : "Директория", + "overwrite" : "Перезаписать", + "go" : "Поехали!", + "log" : "Журнал", + "del_log_rec" : "Удалить запись журнала", + "download_started" : "Загрузка начата" +} diff --git a/system/apps/cai_downloader/readme/en.txt b/system/apps/cai_downloader/readme/en.txt new file mode 100644 index 0000000..1f6fd13 --- /dev/null +++ b/system/apps/cai_downloader/readme/en.txt @@ -0,0 +1,4 @@ +Settings +======== +Set $DataDir variable value to download dir in exec/index.php file. +Grant write access for this directory to web server. diff --git a/system/apps/cai_downloader/readme/ru.txt b/system/apps/cai_downloader/readme/ru.txt new file mode 100644 index 0000000..ecce632 --- /dev/null +++ b/system/apps/cai_downloader/readme/ru.txt @@ -0,0 +1,4 @@ +Настройка +========= +В файле exec/index.php задайте каталог для загрузки файлов в переменной $DataDir. +Права на каталог должны позволять запись веб-серверу. diff --git a/system/apps/index.php b/system/apps/index.php new file mode 100644 index 0000000..6e51d82 --- /dev/null +++ b/system/apps/index.php @@ -0,0 +1,16 @@ +get_avail_json(); +} elseif ($action === 'get_enabled_apps') { + $apps = new \CAI\CAICP\Applications(CP_ROOT_REL); + echo $apps->get_enabled_json(); +} diff --git a/system/apps/nextcloud/app.json b/system/apps/nextcloud/app.json new file mode 100644 index 0000000..80fa949 --- /dev/null +++ b/system/apps/nextcloud/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_nextcloud", + "caption" : "NextCloud", + "http_uri" : "", + "use_http" : false, + "https_uri" : "https://[server_name]/", + "use_https" : true, + "frame_support" : false, + "require" : [ + {"data" : "php"} + ] +} diff --git a/system/apps/nextcloud/icon.png b/system/apps/nextcloud/icon.png new file mode 100644 index 0000000..4121a34 Binary files /dev/null and b/system/apps/nextcloud/icon.png differ diff --git a/system/apps/openfire/app.json b/system/apps/openfire/app.json new file mode 100644 index 0000000..ff1a728 --- /dev/null +++ b/system/apps/openfire/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_openfire", + "caption" : "OpenFire", + "http_uri" : "http://[server_name]:9090/", + "use_http" : true, + "https_uri" : "https://[server_name]:9091/", + "use_https" : true, + "frame_support" : false, + "require" : [ + {"data" : "openfire"} + ] +} diff --git a/system/apps/openfire/icon.png b/system/apps/openfire/icon.png new file mode 100644 index 0000000..d79972f Binary files /dev/null and b/system/apps/openfire/icon.png differ diff --git a/system/apps/owncloud/app.json b/system/apps/owncloud/app.json new file mode 100644 index 0000000..967d674 --- /dev/null +++ b/system/apps/owncloud/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_owncloud", + "caption" : "OwnCloud", + "http_uri" : "http://[server_name]/owncloud/", + "use_http" : true, + "https_uri" : "https://[server_name]/owncloud/", + "use_https" : true, + "frame_support" : true, + "require" : [ + {"data" : "php"} + ] +} diff --git a/system/apps/owncloud/icon.png b/system/apps/owncloud/icon.png new file mode 100644 index 0000000..57d6ff8 Binary files /dev/null and b/system/apps/owncloud/icon.png differ diff --git a/system/apps/phpmyadmin/app.json b/system/apps/phpmyadmin/app.json new file mode 100644 index 0000000..fb090a6 --- /dev/null +++ b/system/apps/phpmyadmin/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_phpmyadmin", + "caption" : "phpMyAdmin", + "http_uri" : "", + "use_http" : true, + "https_uri" : "https://[server_name]/phpMyAdmin/", + "use_https" : true, + "frame_support" : false, + "require" : [ + {"data" : "php"} + ] +} diff --git a/system/apps/phpmyadmin/icon.png b/system/apps/phpmyadmin/icon.png new file mode 100644 index 0000000..eb866ce Binary files /dev/null and b/system/apps/phpmyadmin/icon.png differ diff --git a/system/apps/phppgadmin/app.json b/system/apps/phppgadmin/app.json new file mode 100644 index 0000000..9165817 --- /dev/null +++ b/system/apps/phppgadmin/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_phppgadmin", + "caption" : "phpPgAdmin", + "http_uri" : "", + "use_http" : true, + "https_uri" : "https://[server_name]/phpPgAdmin/", + "use_https" : true, + "frame_support" : true, + "require" : [ + {"data" : "php"} + ] +} diff --git a/system/apps/phppgadmin/icon.png b/system/apps/phppgadmin/icon.png new file mode 100644 index 0000000..e238cd8 Binary files /dev/null and b/system/apps/phppgadmin/icon.png differ diff --git a/system/apps/plex/app.json b/system/apps/plex/app.json new file mode 100644 index 0000000..5e321f7 --- /dev/null +++ b/system/apps/plex/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_plex", + "caption" : "Plex", + "http_uri" : "http://[server_name]:32400/web/index.html", + "use_http" : false, + "https_uri" : "https://[server_name]:32400/web/index.html", + "use_https" : true, + "frame_support" : false, + "require" : [ + {"data" : "plexmediaserver"} + ] +} diff --git a/system/apps/plex/icon.png b/system/apps/plex/icon.png new file mode 100644 index 0000000..355a179 Binary files /dev/null and b/system/apps/plex/icon.png differ diff --git a/system/apps/rslsync/app.json b/system/apps/rslsync/app.json new file mode 100644 index 0000000..9561033 --- /dev/null +++ b/system/apps/rslsync/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_rslsync", + "caption" : "Resilio sync", + "http_uri" : "http://[server_name]:8888/", + "use_http" : true, + "https_uri" : "", + "use_https" : true, + "frame_support" : false, + "require" : [ + {"data" : "rslsync"} + ] +} diff --git a/system/apps/rslsync/icon.png b/system/apps/rslsync/icon.png new file mode 100644 index 0000000..e95f96a Binary files /dev/null and b/system/apps/rslsync/icon.png differ diff --git a/system/apps/transmission/app.json b/system/apps/transmission/app.json new file mode 100644 index 0000000..837c59d --- /dev/null +++ b/system/apps/transmission/app.json @@ -0,0 +1,12 @@ +{ + "app_name" : "app_transmission", + "caption" : "Transmission", + "http_uri" : "http://[server_name]:9091/", + "use_http" : true, + "https_uri" : "", + "use_https" : false, + "frame_support" : true, + "require" : [ + {"data" : "transmission"} + ] +} diff --git a/system/apps/transmission/icon.png b/system/apps/transmission/icon.png new file mode 100644 index 0000000..d9805bd Binary files /dev/null and b/system/apps/transmission/icon.png differ diff --git a/system/classes/CAI/CAICP/src/Applications.php b/system/classes/CAI/CAICP/src/Applications.php new file mode 100644 index 0000000..7524696 --- /dev/null +++ b/system/classes/CAI/CAICP/src/Applications.php @@ -0,0 +1,154 @@ + + * @copyright : Copyright (c) 2017 Alexander I. Chebykin + * @version : 0.9 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +namespace CAI\CAICP; + +class Applications +{ + protected $apps_avail = array(); + protected $apps_enabled = array(); + protected $cp_root = ''; + + /** + * Constructor + * + * @param string $root_dir Control panel's root directory + */ + function __construct($root_dir) + { + $this->cp_root = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $root_dir; + $this->enum_enabled_apps(); + $this->enum_avail_apps(); + } + + /** + * Read available applications info + */ + private function enum_avail_apps() + { + $dir = opendir($this->cp_root . '/system/apps'); + while($file = readdir($dir)) { + if (is_dir($this->cp_root . '/system/apps/' . $file) + && $file != '.' && $file != '..' + && file_exists($this->cp_root . '/system/apps/' . $file . '/app.json')) { + $app_info = json_decode(file_get_contents($this->cp_root . + '/system/apps/' . + $file . + '/app.json'), + true); + if (!array_key_exists('version', $app_info)) { + $app_info['version'] = ''; + } + if (!array_key_exists('author', $app_info)) { + $app_info['author'] = ''; + } + $this->apps_avail[$file] = array('caption' => $app_info['caption'], + 'version' => $app_info['version'], + 'author' => $app_info['author'], + 'enabled' => array_key_exists($app_info['caption'], + $this->apps_enabled)); + } + } + } + + /** + * Returns enabled applications list + * + * @return array enabled applications list + */ + public function enum_enabled_apps() { + $this->apps_enabled = json_decode(file_get_contents($this->cp_root . + '/system/apps/apps.json'), + true); + } + + /** + * Returns available applications list + * + * @return array available applications list + */ + public function get_avail() { + return $this->apps_avail; + } + + /** + * Returns available applications list in json format + * + * @return string available applications list in json format + */ + public function get_avail_json() { + return html_entity_decode(json_encode($this->get_avail())); + } + + /** + * Returns enabled applications list + * + * @return array enabled applications list + */ + public function get_enabled() { + return $this->apps_enabled; + } + + /** + * Returns enabled applications list in json format + * + * @return string enabled applications list + */ + public function get_enabled_json() { + return html_entity_decode(json_encode($this->get_enabled())); + } + + /** + * Enable apps + * + * @param array $values Array with apps + */ + public function enable($values) + { + $this->apps_enabled = $values; + + } + + /** + * Enable apps + * + * @param string $json Apps in JSON format + */ + public function enable_json($json) + { + $this->enable(json_decode($json, true)); + } + + /** + * Set list of enabled applications + * + * @param array $values array with enabled applications + */ + public function set($values) + { + $this->apps_enabled = $values; + } + + /** + * Saves enabled applications list + * + * @return bool + */ + public function save() + { + return file_put_contents($this->cp_root . '/system/apps/apps.json', + html_entity_decode(json_encode($this->apps_enabled))); + } +} diff --git a/system/classes/CAI/CAICP/src/Localization.php b/system/classes/CAI/CAICP/src/Localization.php new file mode 100644 index 0000000..5b67028 --- /dev/null +++ b/system/classes/CAI/CAICP/src/Localization.php @@ -0,0 +1,51 @@ + + * @copyright : Copyright (c) 2017 Alexander I. Chebykin + * @version : 1.0 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +namespace CAI\CAICP; + +class Localization +{ + protected $cp_root; + protected $lang; + protected $locale; + + /** + * Constructor + * + * @param string $root_dir Control panel's root directory + * @param string $settings Control panel settings + */ + function __construct($root_dir, $settings) + { + $this->cp_root = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $root_dir; + + $this->lang = filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_STRING); + if (!array_key_exists($this->lang, $settings['langs'])) { + $this->lang = $settings['lang']; + } + if ($this->lang == '') { + $this->lang = 'en'; + } + $this->locale = json_decode(file_get_contents($this->cp_root . + '/system/json/locale/' . + $this->lang . '.json'), + true); + } + + public function translate($id) + { + return $this->locale[$id]; + } +} diff --git a/system/classes/CAI/CAICP/src/PSR4Autoloader.php b/system/classes/CAI/CAICP/src/PSR4Autoloader.php new file mode 100644 index 0000000..df8f119 --- /dev/null +++ b/system/classes/CAI/CAICP/src/PSR4Autoloader.php @@ -0,0 +1,192 @@ +register(); + * + * // register the base directories for the namespace prefix + * $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src'); + * $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests'); + * + * The following line would cause the autoloader to attempt to load the + * \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php: + * + * prefixes[$prefix]) === false) { + $this->prefixes[$prefix] = array(); + } + + // retain the base directory for the namespace prefix + if ($prepend) { + array_unshift($this->prefixes[$prefix], $base_dir); + } else { + array_push($this->prefixes[$prefix], $base_dir); + } + } + + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * @return mixed The mapped file name on success, or boolean false on + * failure. + */ + public function loadClass($class) + { + // the current namespace prefix + $prefix = $class; + + // work backwards through the namespace names of the fully-qualified + // class name to find a mapped file name + while (false !== $pos = strrpos($prefix, '\\')) { + + // retain the trailing namespace separator in the prefix + $prefix = substr($class, 0, $pos + 1); + + // the rest is the relative class name + $relative_class = substr($class, $pos + 1); + + // try to load a mapped file for the prefix and relative class + $mapped_file = $this->loadMappedFile($prefix, $relative_class); + if ($mapped_file) { + return $mapped_file; + } + + // remove the trailing namespace separator for the next iteration + // of strrpos() + $prefix = rtrim($prefix, '\\'); + } + + // never found a mapped file + return false; + } + + /** + * Load the mapped file for a namespace prefix and relative class. + * + * @param string $prefix The namespace prefix. + * @param string $relative_class The relative class name. + * @return mixed Boolean false if no mapped file can be loaded, or the + * name of the mapped file that was loaded. + */ + protected function loadMappedFile($prefix, $relative_class) + { + // are there any base directories for this namespace prefix? + if (isset($this->prefixes[$prefix]) === false) { + return false; + } + + // look through base directories for this namespace prefix + foreach ($this->prefixes[$prefix] as $base_dir) { + + // replace the namespace prefix with the base directory, + // replace namespace separators with directory separators + // in the relative class name, append with .php + $file = $base_dir + . str_replace('\\', '/', $relative_class) + . '.php'; + + // if the mapped file exists, require it + if ($this->requireFile($file)) { + // yes, we're done + return $file; + } + } + + // never found it + return false; + } + + /** + * If a file exists, require it from the file system. + * + * @param string $file The file to require. + * @return bool True if the file exists, false if not. + */ + protected function requireFile($file) + { + if (file_exists($file)) { + require $file; + return true; + } + return false; + } +} diff --git a/system/classes/CAI/CAICP/src/Settings.php b/system/classes/CAI/CAICP/src/Settings.php new file mode 100644 index 0000000..572619d --- /dev/null +++ b/system/classes/CAI/CAICP/src/Settings.php @@ -0,0 +1,123 @@ + + * @copyright : Copyright (c) 2017 Alexander I. Chebykin + * @version : 0.9 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +namespace CAI\CAICP; + +class Settings +{ + protected $user_settings = array(); + protected $cp_root = ''; + + /** + * Constructor + * + * @param string $root_dir Control panel's root directory + */ + function __construct($root_dir) + { + $this->cp_root = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $root_dir; + $this->load(); + } + + /** + * Read settings from disk + */ + private function load() + { + $this->user_settings = json_decode( + file_get_contents($this->cp_root . '/system/json/settings.json'), + true); + } + + /** + * Save settings to disk + * + * @return false|written bytes count + */ + public function save() + { + return file_put_contents($this->cp_root . '/system/json/settings.json', + html_entity_decode(json_encode($this->user_settings))); + } + + /** + * Return array with settings + * + * @return array + */ + public function get() + { + return $this->user_settings; + } + + /** + * Return string with settings in JSON format + * + * @return string + */ + public function get_json() + { + return html_entity_decode(json_encode($this->get())); + } + + /** + * Return option value + * + * @param string $option_name Option name + * + * @return variant|boolean + */ + public function get_value($option_name) + { + if (trim($option_name) !== '') { + return $this::user_settings[$option_name]; + } else { + return false; + } + } + + /** + * Set all settings + * + * @param array $values Array with settings + */ + public function set($values) + { + $this->user_settings = $values; + } + + /** + * Set all settings + * + * @param string $json Settings in JSON format + */ + public function set_json($json) + { + $this->set(json_decode($json, true)); + } + + /** + * Set option value + * + * @param string $option_name Option name + * @param variant $value New value + */ + public function set_value($option_name, $value) + { + if (trim($option_name) !== '') { + $this->user_settings[$option_name] = $value; + } + } +} diff --git a/system/help/css/about.css b/system/help/css/about.css new file mode 100644 index 0000000..b274a99 --- /dev/null +++ b/system/help/css/about.css @@ -0,0 +1,32 @@ +.hidden {display: none;} +.centered {text-align: center;} + +.help_title {font-weight: bold;} + +.help_ver, .span_ver, +.span_ver_date {font-style: italic;} + +.span_author, +.span_author_ru {} + +html, +body { + height: 100%; + margin: 0; +} + +hr { + margin: 5px 0; + padding: 0; + border: 0; + height: 1px; + background-image: -webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background: linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0, 0)); +} + +article { + margin: 10px; +} \ No newline at end of file diff --git a/system/help/css/help.css b/system/help/css/help.css new file mode 100644 index 0000000..9659c91 --- /dev/null +++ b/system/help/css/help.css @@ -0,0 +1,89 @@ +.hidden {display: none;} +.centered {text-align: center;} + +.help_title {font-weight: bold;} + +.help_ver, .span_ver, +.span_ver_date {font-style: italic;} + +.tbl_wide { + width: 100%; + border: solid 1px #ccc; +} +.tbl_wide .tbl_header { + font-weight: bold; + text-align: center; + background-color: #ccc; +} +.tbl_wide td { + padding: 5px; + border: solid 1px #ccc; +} + +.mnu_selected { + background-color: #000; + color: #fff; +} + +.scrollable {overflow: auto;} + +html, +body { + height: 100%; + margin: 0; +} + +hr { + margin: 5px 0; + padding: 0; + border: 0; + height: 1px; + background-image: -webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0)); + background: linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0, 0)); +} + +nav { + width: 200px; + box-shadow: 5px 5px 5px #ccc; + position: fixed; + padding: 2px; + margin-left: 10px; + border: solid 1px #ccc; +} +nav ul { + padding: 0; + margin: 2px; + list-style: none; + transition: all 0.5s ease-out 0.1s; +} +nav ul li { + cursor: pointer; +} +nav ul li ul { + padding: 0 0 0 2px; +} +nav ul li div { + padding: 2px 2px 2px 4px; +} +nav ul li div:hover { + background-color: #ddd; + color: #000; +} + +article { + margin: 2px 2px 2px 225px; + transition: all 0.5s ease-out 0.1s; +} + +a, a:visited, a:hover { + color: #555; + text-decoration: none; +} +a:hover {text-decoration: underline;} + +#div_license { + height: 150px; +} \ No newline at end of file diff --git a/system/help/index.html b/system/help/index.html new file mode 100644 index 0000000..4ca3308 --- /dev/null +++ b/system/help/index.html @@ -0,0 +1,13 @@ + + + + CAI CP: Help + + + + + + Loading... + + + diff --git a/system/help/js/help.js b/system/help/js/help.js new file mode 100644 index 0000000..6a681ed --- /dev/null +++ b/system/help/js/help.js @@ -0,0 +1,201 @@ +// MIT License: +// +// Copyright (c) 2016-2017, Alexander I. Chebykin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * CAI CP v.1 + * + * @module : Help subsystem + * @author : Alexander I. Chebykin + * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin + * @version : 0.9 + * @build date : 2017-06-16 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +(function () { + 'use strict'; + + /** + * Return GET parameter + * + * @param {string} key Key name + * + * returns {string} + */ + function $_GET(key) { + var s = window.location.search; + + s = s.match(new RegExp(key + '=([^&=]+)')); + + return s ? s[1] : false; + } + + /** + * HelpSystem constructor + * + * @returns {undefined} + */ + function HelpSystem() { + this.locale = 'en'; + } + + /** + * + * @param {int} init_level Initialization level: 1 - select locale + * 2 - redirect to localized resource + * 3 - load version info + * 4 - set event handlers + * + * @returns {undefined} + */ + HelpSystem.prototype.init = function (init_level) { + var help_instance = this; + + switch (init_level) { + case 1: + if ($_GET('lang') !== '') { + this.locale = $_GET('lang'); + } else { + var request = new XMLHttpRequest(), + res_data; + + if (!~location.href.indexOf('locale')) { + request.open('GET', '../json/settings.json'); + } else { + request.open('GET', '../../../json/settings.json'); + } + request.send(); + + request.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + res_data = JSON.parse(this.responseText); + + help_instance.locale = res_data.lang; + } + } + }; + } + this.init(2); + break; + case 2: + if (!~location.href.indexOf('locale')) { + location.href = 'locale/' + this.locale + location.hash; + return; + } else { + this.init(3); + } + break; + case 3: + var request = new XMLHttpRequest(), + res_data; + + if (!~location.href.indexOf('locale')) { + request.open('GET', '../json/version.json'); + } else { + request.open('GET', '../../../json/version.json'); + } + request.send(); + + request.onreadystatechange = function (e) { + if (this.readyState === 4) { + if (this.status === 200) { + res_data = JSON.parse(this.responseText); + + var vers = document.getElementsByClassName('span_ver'), + ver_dates = document.getElementsByClassName('span_ver_date'); + + for (var i = 0; i < vers.length; i++) { + vers[i].textContent = res_data.version; + } + for (var i = 0; i < ver_dates.length; i++) { + ver_dates[i].textContent = new Date(res_data.build_date).toLocaleDateString(); + } + } + } + }; + this.init(4); + break; + case 4: + var nav_list = document.getElementsByTagName('nav'), + toc_lis = document.getElementById('menu_toc').getElementsByTagName('li'); + + // Show menu + for (var i = 0; i < nav_list.length; i++) { + nav_list[i].classList.remove('hidden'); + } + + // Add event listeners + for (var i = 0; i < toc_lis.length; i++) { + toc_lis[i].getElementsByTagName('div')[0].addEventListener('click', function (event) { + help_instance.select_topic(this.parentElement.id.substr(4)); + }); + } + + if (location.hash !== '') { + help_instance.select_topic(location.hash.substr(1)); + } else { + help_instance.select_topic('start'); + } + } + }; + + /** + * + * @param {string} topic_id Topic identifier + * + * @returns {undefined} + */ + HelpSystem.prototype.select_topic = function (topic_id) { + var articles = document.getElementsByTagName('article'), + toc_divs = document.getElementsByClassName('div_toc'); + + for (var j = 0; j < toc_divs.length; j++) { + toc_divs[j].classList.remove('mnu_selected'); + } + + document.getElementById('mnu_' + topic_id).getElementsByTagName('div')[0].classList.add('mnu_selected'); + + for (var j = 0; j < articles.length; j++) { + if (articles[j].id === 'art_' + topic_id) { + articles[j].classList.remove('hidden'); + } else { + articles[j].classList.add('hidden'); + } + } + location.hash = topic_id; + }; + + document.addEventListener('DOMContentLoaded', function () { + 'use strict'; + + /** + * Initialize help object + */ + var help = new HelpSystem(); + + help.init(1); + + }); + +}()); diff --git a/system/help/locale/en/index.html b/system/help/locale/en/index.html new file mode 100644 index 0000000..95eb1e5 --- /dev/null +++ b/system/help/locale/en/index.html @@ -0,0 +1,230 @@ + + + + CAI CP: Help + + + + + + + + + + + + + + diff --git a/system/help/locale/ru/index.html b/system/help/locale/ru/index.html new file mode 100644 index 0000000..fd4e8f6 --- /dev/null +++ b/system/help/locale/ru/index.html @@ -0,0 +1,268 @@ + + + + CAI CP: Help + + + + + + + + + + + + + + diff --git a/system/json/general_settings/index.php b/system/json/general_settings/index.php new file mode 100644 index 0000000..b2e4c2a --- /dev/null +++ b/system/json/general_settings/index.php @@ -0,0 +1,9 @@ + 440, + 'LICENSE' => 440, + 'css' => 550, + 'css/core.css' => 440, + 'css/core.min.css' => 440, + 'gfx' => 550, + 'gfx/bg.jpg' => 440, + 'gfx/menu.png' => 440, + 'gfx/buttons' => 550, + 'gfx/buttons/delete.png' => 440, + 'gfx/distros' => 550, + 'gfx/distros/apple.png' => 440, + 'gfx/distros/arch.png' => 440, + 'gfx/distros/bsd.png' => 440, + 'gfx/distros/debian.png' => 440, + 'gfx/distros/fedora.png' => 440, + 'gfx/distros/linux.png' => 440, + 'gfx/distros/opensuse.png' => 440, + 'gfx/distros/ubuntu.png' => 440, + 'gfx/distros/unknown.png' => 440, + 'gfx/flags' => 550, + 'gfx/flags/russian-16x16.png' => 440, + 'gfx/flags/uk-16x16.png' => 440, + 'gfx/icons' => 550, + 'gfx/icons/about.png' => 440, + 'gfx/icons/help.png' => 440, + 'gfx/icons/loading.gif' => 440, + 'gfx/icons/network.png' => 440, + 'gfx/icons/server.png' => 440, + 'gfx/icons/settings.png' => 440, + 'gfx/icons/samba.png' => 440, + 'gfx/icons/storage.png' => 440, + 'gfx/icons/sysinfo.png' => 440, + 'js' => 550, + 'js/core.js' => 440, + 'js/smoothie.js' => 440, + 'system' => 550, + 'system/settings.php' => 440, + 'system/action' => 550, + 'system/action/index.php' => 440, + 'system/apps' => 550, + 'system/apps/apps.json' => 640, + 'system/apps/cai_downloader' => 550, + 'system/apps/cai_downloader/app.json' => 440, + 'system/apps/cai_downloader/icon.png' => 440, + 'system/apps/cai_downloader/index.html' => 440, + 'system/apps/cai_downloader/css' => 550, + 'system/apps/cai_downloader/css/downloader.css' => 440, + 'system/apps/cai_downloader/exec' => 550, + 'system/apps/cai_downloader/exec/index.php' => 440, + 'system/apps/cai_downloader/exec/scripts' => 550, + 'system/apps/cai_downloader/exec/scripts/app.sh' => 550, + 'system/apps/cai_downloader/js' => 550, + 'system/apps/cai_downloader/js/downloader.js' => 440, + 'system/apps/cai_downloader/locale' => 550, + 'system/apps/cai_downloader/locale/en.json' => 440, + 'system/apps/cai_downloader/locale/ru.json' => 440, + 'system/apps/nextcloud' => 550, + 'system/apps/nextcloud/app.json' => 440, + 'system/apps/nextcloud/icon.png' => 440, + 'system/apps/openfire' => 550, + 'system/apps/openfire/app.json' => 440, + 'system/apps/openfire/icon.png' => 440, + 'system/apps/owncloud' => 550, + 'system/apps/owncloud/app.json' => 440, + 'system/apps/owncloud/icon.png' => 440, + 'system/apps/phpmyadmin' => 550, + 'system/apps/phpmyadmin/app.json' => 440, + 'system/apps/phpmyadmin/icon.png' => 440, + 'system/apps/phppgadmin' => 550, + 'system/apps/phppgadmin/app.json' => 440, + 'system/apps/phppgadmin/icon.png' => 440, + 'system/apps/plex' => 550, + 'system/apps/plex/app.json' => 440, + 'system/apps/plex/icon.png' => 440, + 'system/apps/rslsync' => 550, + 'system/apps/rslsync/app.json' => 440, + 'system/apps/rslsync/icon.png' => 440, + 'system/apps/transmission' => 550, + 'system/apps/transmission/app.json' => 440, + 'system/apps/transmission/icon.png' => 440, + 'system/classes' => 550, + 'system/classes/CAI' => 550, + 'system/classes/CAI/CAICP' => 550, + 'system/classes/CAI/CAICP/src' => 550, + 'system/classes/CAI/CAICP/src/Applications.php' => 440, + 'system/classes/CAI/CAICP/src/Localization.php' => 440, + 'system/classes/CAI/CAICP/src/PSR4Autoloader.php' => 440, + 'system/classes/CAI/CAICP/src/Settings.php' => 440, + 'system/help' => 550, + 'system/help/index.html' => 440, + 'system/help/css' => 550, + 'system/help/css/help.css' => 440, + 'system/help/js' => 550, + 'system/help/js/help.js' => 440, + 'system/help/locale' => 550, + 'system/help/locale/en' => 550, + 'system/help/locale/en/index.html' => 440, + 'system/help/locale/ru' => 550, + 'system/help/locale/ru/index.html' => 440, + 'system/json' => 550, + 'system/json/index.php' => 440, + 'system/json/settings.json' => 640, + 'system/json/version.json' => 440, + 'system/json/general_settings' => 550, + 'system/json/general_settings/index.php' => 440, + 'system/json/locale' => 550, + 'system/json/locale/en.json' => 440, + 'system/json/locale/ru.json' => 440, + 'system/scripts' => 550, + 'system/scripts/check_files.php' => 440, + 'system/scripts/index.php' => 440, + 'system/scripts/json_api_nix.sh' => 550, + 'system/scripts/json_transmission.php' => 440, + 'system/scripts/json_transmission.sh' => 550, + 'system/scripts/smart_temp.sh' => 550, + 'system/scripts/trim.awk' => 440, + 'system/ui' => 550, + 'system/ui/forms' => 550, + 'system/ui/forms/settings' => 550, + 'system/ui/forms/settings/index.html' => 440, + 'system/ui/forms/settings/css' => 550, + 'system/ui/forms/settings/css/styles.css' => 440, + 'system/ui/forms/settings/js' => 550, + 'system/ui/forms/settings/js/settings.js' => 440 + ); +$script_response = array(); +$err_count = 0; + +foreach ($files as $key => $value) { + if (!file_exists($cp_root . '/' . $key)) { + $err_count += 1; + + $script_response[] = array( 'file' => $key, + 'exists' => 0, + 'mask_required' => 0, + 'mask_actual' => 0); + } else { + $file_owner = posix_getpwuid(fileowner($cp_root . '/' . $key))['name']; + $file_group = posix_getgrgid(filegroup($cp_root . '/' . $key))['name']; + $file_mask = (int)substr(sprintf('%o', + fileperms($cp_root . '/' . $key) + ), + -3); + + if (($value != $file_mask) || + ($file_group != FILE_GROUP) || + $file_owner != FILE_OWNER) { + $err_count += 1; + + $script_response[] = array( 'file' => $key, + 'exists' => 1, + 'mask_required' => $value, + 'mask_actual' => $file_mask, + 'owner_required' => FILE_OWNER, + 'owner_actual' => $file_owner, + 'group_required' => FILE_GROUP, + 'group_actual' => $file_group); + } + } +} + +echo html_entity_decode(json_encode($script_response)); diff --git a/system/scripts/index.php b/system/scripts/index.php new file mode 100644 index 0000000..f4ef8d6 --- /dev/null +++ b/system/scripts/index.php @@ -0,0 +1,30 @@ + +# @copyright : Copyright (c) 2016-2018 Alexander I. Chebykin +# @version : 0.9.1 +# @build date : 2018-08-23 +# @license : MIT +# @link : https://github.com/CAI79/CAI-CP +################################################################# + +skeleton() { + cmd_arp=`which arp` + cmd_awk=`which awk` + cmd_cat=`which cat` + cmd_date=`which date` + cmd_df=`which df` + cmd_free=`which free` + cmd_grep=`which grep` + cmd_hostname=`which hostname` + cmd_iostat=`which iostat` + cmd_ip=`which ip` + cmd_lsb_release=`which lsb_release` + cmd_lscpu=`which lscpu` + cmd_netstat=`which netstat` + cmd_ps=`which ps` + cmd_sed=`which sed` + cmd_smartctl=`which smartctl` + cmd_smbstatus=`which smbstatus` + cmd_sort=`which sort` + cmd_ss=`which ss` + cmd_uname=`which uname` + cmd_w=`which w` + + result=$() + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +_print_parsed() { + while read data; do +# echo -n "$data" | sed -r "s/\"/\\\\\"/g" | tr -d "\n"; + echo -n "$data" | tr -d "\n"; + done; +} + +######################### +# Common info functions # +######################### +cpu() { + cmd_awk=`which awk` + cmd_cat=`which cat` + + result=$({ $cmd_cat /proc/stat; sleep "1"; $cmd_cat /proc/stat; } \ + | $cmd_awk '/^cpu* / {usr=$2-usr; sys=$4-sys; idle=$5-idle; iow=$6-iow} END \ + {total=usr+sys+idle+iow; printf "%.2f\n", (total-idle)*100/total}' \ + | $cmd_awk -F " " '{print "{\n \"cpu\": " $1 "\n}"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# CPU(s) count +cpu_count() { + cmd_awk=`which awk` + cmd_grep=`which grep` + cmd_lscpu=`which lscpu` + + result=$($cmd_lscpu \ + | $cmd_grep '^CPU(s):' \ + | $cmd_awk '{ print "{\n\t\"cpu(s)\": " $2 "\n}" }') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# CPU(s) load information +cpu_load() { + cmd_awk=`which awk` + cmd_cat=`which cat` + cmd_grep=`which grep` + cmd_lscpu=`which lscpu` + + cpus=$($cmd_lscpu \ + | $cmd_grep '^CPU(s):' \ + | $cmd_awk '{ print $2 }') + + i=0 + + while [ $i -lt $cpus ] + do + if [ "$i" -ne 0 ]; then + result="$result," + fi + result+=$( + { $cmd_cat /proc/stat; sleep "1"; $cmd_cat /proc/stat; } \ + | $cmd_awk '/^cpu'$i' / {usr=$2-usr; sys=$4-sys; idle=$5-idle; iow=$6-iow} END \ + {total=usr+sys+idle+iow; printf "%.2f\n", (total-idle)*100/total}' \ + | $cmd_awk -F " " '{print "\n \"cpu'$i'\": " int($1) "\n"}' + ) + (( i++ )) + done +echo "{$result}" | _print_parsed +} + +# CPU information +cpu_info() { + cmd_awk=`which awk` + cmd_lscpu=`which lscpu` + + result=$($cmd_lscpu \ + | $cmd_awk -F: '{print "\""$1"\": \""$2"\"," }'\ + ) + + echo "[{" ${result%?} "}]" | _print_parsed +} + +# General OS information +general_info() { + cmd_awk=`which awk` + cmd_cat=`which cat` + cmd_date=`which date` + cmd_hostname=`which hostname` + cmd_lsb_release=`which lsb_release` + cmd_sed=`which sed` + cmd_uname=`which uname` + + function displaytime { + local T=$1 + local D=$((T/60/60/24)) + local H=$((T/60/60%24)) + local M=$((T/60%60)) + local S=$((T%60)) + [[ $D > 0 ]] && printf '%d days ' $D + [[ $H > 0 ]] && printf '%d hours ' $H + [[ $M > 0 ]] && printf '%d minutes ' $M + [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and ' + printf '%d seconds\n' $S + } + + lsbRelease=$($cmd_lsb_release -ds | $cmd_sed -e 's/^"//' -e 's/"$//') + uname=$($cmd_uname -r | $cmd_sed -e 's/^"//' -e 's/"$//') + os=`echo $lsbRelease $uname` + hostname=$($cmd_hostname) + uptime_seconds=$($cmd_cat /proc/uptime | $cmd_awk '{print $1}') + server_time=$($cmd_date) + + echo "[{ \"OS\": \"$os\", \ + \"Hostname\": \"$hostname\", \ + \"Uptime\": \" $(displaytime ${uptime_seconds%.*}) \", \ + \"Server Time\": \"$server_time\" }]" \ + | _print_parsed +} + +# OS distriboutive info +os_distr() { + cmd_lsb_release=`which lsb_release` + cmd_sed=`which sed` + cmd_uname=`which uname` + + lsbRelease=$($cmd_lsb_release -ds | $cmd_sed -e 's/^"//' -e 's/"$//') + osFamily=$($cmd_uname) + + echo "{ \"Distr\": \"$lsbRelease\", \ + \"Family\": \"$osFamily\" }" \ + | _print_parsed +} + +# Running processes list +processes() { + cmd_awk=`which awk` + cmd_ps=`which ps` + cmd_sed=`which sed` + cmd_sort=`which sort` + + result=$($cmd_ps -eafw \ + | $cmd_sed -e '1d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"uid\": \"%s\", \ + \n\t\t\"pid\": %s, \ + \n\t\t\"ppid\": %s, \ + \n\t\t\"c\": %s, \ + \n\t\t\"stime\": \"%s\", \ + \n\t\t\"tty\": \"%s\", \ + \n\t\t\"time\": \"%s\", \ + \n\t\t\"cmd\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, $8)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Server name +srv_name() { + cmd_awk=`which awk` + cmd_uname=`which uname` + + result=$($cmd_uname -n | $cmd_awk '{ print "{\n\t\"server_name\": \"" $1 "\"\n}" }') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +############################## +# File system info functions # +############################## + +# File system info +fs() { + cmd_awk=`which awk` + cmd_df=`which df` + cmd_sed=`which sed` + cmd_sort=`which sort` + + result=$($cmd_df -h -T \ + | $cmd_sed -e '1d' \ + | $cmd_sort -k1,1 -k7,7 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"fs\": \"%s\", \ + \n\t\t\"type\": \"%s\", \ + \n\t\t\"size\": \"%s\", \ + \n\t\t\"used\": \"%s\", \ + \n\t\t\"free\": \"%s\", \ + \n\t\t\"percent\": \"%s\", \ + \n\t\t\"mount\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7)} \ + END{print "\n]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# HDD S.M.A.R.T. status +hdd_smart() { + cmd_awk=`which awk` + cmd_sed=`which sed` + cmd_smartctl=`which smartctl` + + result=$(sudo $cmd_smartctl --scan \ + | $cmd_awk '{printf("%s ", $1)} {system("sudo smartctl -H \"" $1 "\" \ + | sed -n '5p'")}' \ + | $cmd_sed -e '/^$/d' \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"drive\": \"%s\",\n\t\t\"status\": \"%s\"\n\t}", $1, $NF)} \ + END \ + {print "\n]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# HDD temperature info +hdd_temp() { + cmd_awk=`which awk` + cmd_smartctl=`which smartctl` + + path=`pwd` + + result=$($cmd_smartctl --scan \ + | $cmd_awk '{system("./smart_temp.sh \"" $1 "\"")}' \ + | $cmd_awk -F: 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {FS=": "} \ + {printf("\t\t\"drive\": \"%s\", \ + \n\t\t\"model\": \"%s\", \ + \n\t\t\"temperature\": \"%s\"\n\t}", $1, $2, $3)} \ + END\ + {print "\n]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# IO statistics +io_stat() { + cmd_awk=`which awk` + cmd_iostat=`which iostat` + cmd_sed=`which sed` + cmd_sort=`which sort` + + result=$($cmd_iostat \ + | $cmd_sed -e '1,6d;/^$/d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"device\": \"%s\", \ + \n\t\t\"tps\": %s, \ + \n\t\t\"kB_read/s\": %s, \ + \n\t\t\"kB_wrtn/s\": %s, \ + \n\t\t\"kB_read\": %s, \ + \n\t\t\"kB_wrtn\": %s\n\t}", $1, $2, $3, $4, $5, $6)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +######################### +# Memory info functions # +######################### + +# General memory information +mem_info() { + cmd_awk=`which awk` + cmd_cat=`which cat` + cmd_sed=`which sed` + + result=$($cmd_cat /proc/meminfo \ + | $cmd_awk -F: 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 "\"," } END {print "}"}' \ + | $cmd_sed 'N;$s/,\n/\n/;P;D') + + echo "[" ${result%?} "}]" | _print_parsed +} + +# Memory usage information +# +# Based on Linux Dash codebase: https://github.com/afaqurk/linux-dash +# +# References: +# Calculations: http://zcentric.com/2012/05/29/mapping-procmeminfo-to-output-of-free-command/ +# Fields: https://www.kernel.org/doc/Documentation/filesystems/proc.txt +mem() { + cmd_awk=`which awk` + cmd_cat=`which cat` + cmd_grep=`which grep` + + memInfoFile="/proc/meminfo" + + memInfo=`$cmd_cat $memInfoFile | $cmd_grep 'MemTotal\|MemFree\|Buffers\|Cached'` + + echo $memInfo \ + | $cmd_awk '{print "{ \"total\": " ($2/1024) ", \"used\": " ( ($2-($5+$8+$11))/1024 ) " }" }' \ + | _print_parsed +} + +# Swap memory info +swap() { + cmd_awk=`which awk` + cmd_free=`which free` + cmd_sed=`which sed` + +# result=$($cmd_free -m \ +# | $cmd_sed -e '1d' -e '2d' -e '3d' \ +# | $cmd_awk 'BEGIN \ +# {print "{"} \ +# {if (NR > 1) print ","} \ +# {printf("\n\t\"total\": %s, \n\t\"used\": %s", $2, $3)} \ +# END{print "\n}"}') + result=$($cmd_free -m \ + | $cmd_sed '/Swap:/!d' \ + | $cmd_awk 'BEGIN \ + {print "{"} \ + {if (NR > 1) print ","} \ + {printf("\n\t\"total\": %s, \n\t\"used\": %s", $2, $3)} \ + END{print "\n}"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +########################## +# Network info functions # +########################## + +# ARP cache table +# +# Based on Linux Dash codebase: https://github.com/afaqurk/linux-dash +arp_cache() { + cmd_awk=`which awk` + cmd_sed=`which sed` + if hash arp 2>/dev/null; then cmd_arp=`which arp` + else cmd_arp="ip neigh" + fi + + result=$($cmd_arp | \ + $cmd_awk 'BEGIN {print "["} NR>1 \ + {if ( NF==6 ) {print "{ \"addr\": \"" $1 "\", " \ + "\"hw_type\": \"" $2 "\", " \ + "\"hw_addr\": \"" $3 "\", " \ + "\"flags\": \"" $4 "\", " \ + "\"mask\": \"" $5 "\" , " \ + "\"iface\": \"" $6 "\" }, "} \ + if ( NF==5 ) {print "{ \"addr\": \"" $1 "\", " \ + "\"hw_type\": \"" $2 "\", " \ + "\"hw_addr\": \"" $3 "\", " \ + "\"flags\": \"" $4 "\", " \ + "\"mask\": \"\", " \ + "\"iface\": \"" $5 "\" }, "} \ + if ( NF==4 ) {print "{ \"addr\": \"" $1 "\", " \ + "\"hw_type\": \"" $2 "\", " \ + "\"hw_addr\": \"" $3 "\", " \ + "\"flags\": \"\", " \ + "\"mask\": \"\", " \ + "\"iface\": \"" $4 "\" }, "} \ + if ( NF==3 ) {print "{ \"addr\": \"" $1 "\", " \ + "\"hw_type\": \"\", " \ + "\"hw_addr\": \"" $2 "\", " \ + "\"flags\": \"\", " \ + "\"mask\": \"\", " \ + "\"iface\": \"" $3 "\" }, "} \ + } \ + END {print "]"}' \ + | $cmd_sed 'N;$s/},/}/;P;D') + + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# IP addresses +# +# Based on Linux Dash codebase: https://github.com/afaqurk/linux-dash +ip_addr() { + cmd_awk=`which awk` + cmd_grep=`which grep` + cmd_ifconfig=`which ifconfig` + cmd_tr=`which tr` + cmd_dig=`which dig` + + externalIp=`$cmd_dig +short myip.opendns.com @resolver1.opendns.com` + + $cmd_ifconfig \ + | $cmd_grep -B1 "inet addr" \ + | $cmd_awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' \ + | $cmd_awk -v exIp="$externalIp" -F: 'BEGIN {print "["} { print "{ \"interface\": \"" $1 "\", \"ip\": \"" $3 "\" },"} END {print "{ \"interface\": \"external\", \"ip\": \""exIp"\" } ]"}' \ + | $cmd_tr -d '\r\n' +} + +# Network bandwith +net_band() { + cmd_awk=`which awk` + cmd_cat=`which cat` + cmd_sed=`which sed` + + result=$($cmd_cat /proc/net/dev \ + | $cmd_awk 'BEGIN\ + {print "["} NR>2 \ + {print "{ \"interface\": \"" $1 "\"," " \"tx\": " $2 "," " \"rx\": " $10 " }," } \ + END\ + {print "]"}' \ + | $cmd_sed 'N;$s/,\n/\n/;P;D') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Network connections +net_conn() { + cmd_awk=`which awk` + cmd_sed=`which sed` + cmd_sort=`which sort` + cmd_ss=`which ss` + + result=$($cmd_ss \ + | $cmd_sed -e '1d;/u_str/d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"netid\": \"%s\", \ + \n\t\t\"state\": \"%s\", \ + \n\t\t\"rcvq\": %s, \ + \n\t\t\"sendq\": %s, \ + \n\t\t\"local\": \"%s\", \ + \n\t\t\"peer\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6)} \ + END{print "\n]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Download transfer rate +# +# Based on Linux Dash codebase: https://github.com/afaqurk/linux-dash +net_download_transfer_rate() { + files=(/sys/class/net/*) + pos=$(( ${#files[*]} - 1 )) + last=${files[$pos]} + + json_output="{" + + for interface in "${files[@]}" + do + basename=$(basename "$interface") + + # find the number of bytes transfered for this interface + in1=$(cat /sys/class/net/"$basename"/statistics/rx_bytes) + + # wait a second + sleep 1 + + # check same interface again + in2=$(cat /sys/class/net/"$basename"/statistics/rx_bytes) + + # get the difference (transfer rate) + in_bytes=$((in2 - in1)) + + # convert transfer rate to KB + in_kbytes=$((in_bytes / 1024)) + + # convert transfer rate to KB + json_output="$json_output \"$basename\": $in_kbytes" + + # if it is not the last line + if [[ ! $interface == $last ]] + then + # add a comma to the line (JSON formatting) + json_output="$json_output," + fi + done + + # close the JSON object & print to screen + echo "$json_output}" | _print_parsed +} + +# Upload transfer rate +# +# Based on Linux Dash codebase: https://github.com/afaqurk/linux-dash +net_upload_transfer_rate() { + files=(/sys/class/net/*) + pos=$(( ${#files[*]} - 1 )) + last=${files[$pos]} + + json_output="{" + + for interface in "${files[@]}" + do + basename=$(basename "$interface") + + # find the number of bytes transfered for this interface + out1=$(cat /sys/class/net/"$basename"/statistics/tx_bytes) + + # wait a second + sleep 1 + + # check same interface again + out2=$(cat /sys/class/net/"$basename"/statistics/tx_bytes) + + # get the difference (transfer rate) + out_bytes=$((out2 - out1)) + + # convert transfer rate to KB + out_kbytes=$((out_bytes / 1024)) + + # convert transfer rate to KB + json_output="$json_output \"$basename\": $out_kbytes" + + # if it is not the last line + if [[ ! $interface == $last ]] + then + # add a comma to the line (JSON formatting) + json_output="$json_output," + fi + done + + # close the JSON object & print to screen + echo "$json_output}" | _print_parsed +} + +# Online users info +users_online() { + cmd_awk=`which awk` + cmd_netstat=`which netstat` + cmd_sed=`which sed` + cmd_smbstatus=`which smbstatus` + cmd_sort=`which sort` + cmd_w=`which w` + + result=$($cmd_w \ + | $cmd_sed -e '1,2d' \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"user\": \"%s\", \ + \n\t\t\"tty\": \"%s\", \ + \n\t\t\"from\": \"%s\", \ + \n\t\t\"login\": \"%s\", \ + \n\t\t\"idle\": \"%s\", \ + \n\t\t\"jcpu\": \"%s\", \ + \n\t\t\"pcpu\": \"%s\", \ + \n\t\t\"what\": \"%s %s %s\"\n\t}", \ + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)} \ + END \ + {print "\n]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# IP routing table +ip_route() { + if hash netstat 2>/dev/null; then ip_route_netstat + else ip_route_ip + fi +} + +# IP routing table (ip) +ip_route_ip() { + cmd_awk=`which awk` + cmd_ip=`which ip` + cmd_sed=`which sed` + + result=$($cmd_ip r \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{\"route\": \""} \ + {print } \ + {print "\"\n\t}"} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# IP routing table (netstat) +ip_route_netstat() { + cmd_awk=`which awk` + cmd_netstat=`which netstat` + cmd_sed=`which sed` + + result=$($cmd_netstat -r \ + | $cmd_sed -e '1,2d' \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"destination\": \"%s\", \ + \n\t\t\"gateway\": \"%s\", \ + \n\t\t\"genmask\": \"%s\", \ + \n\t\t\"flags\": \"%s\", \ + \n\t\t\"mss\": %s, \ + \n\t\t\"window\": %s, \ + \n\t\t\"irtt\": %s, \ + \n\t\t\"iface\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, $8)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Multicast groups +net_multicast() { + cmd_awk=`which awk` + cmd_netstat=`which netstat` + cmd_sed=`which sed` + cmd_sort=`which sort` + + result=$($cmd_netstat -g \ + | $cmd_sed -e '1,3d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"interface\": \"%s\", \ + \n\t\t\"refcnt\": %s, \ + \n\t\t\"group\": \"%s\"\n\t}", $1, $2, $3)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Listening sockets +listen_socks() { + if hash netstat 2>/dev/null; then listen_socks_netstat + else listen_socks_ss + fi +} + +# Listening sockets (ss) +listen_socks_ss() { + cmd_awk=`which awk` + cmd_ss=`which ss` + cmd_sed=`which sed` + + result=$($cmd_ss -ltn \ + | $cmd_sed -e '1d' \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"state\": \"%s\", \ + \n\t\t\"recv-q\": %s, \ + \n\t\t\"send-q\": %s, \ + \n\t\t\"local address:port\": \"%s\", \ + \n\t\t\"peer addr:port\": \"%s\"\n\t}", $1, $2, $3, $4, $5)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Listening sockets (netstat) +listen_socks_netstat() { + cmd_awk=`which awk` + cmd_netstat=`which netstat` + cmd_sed=`which sed` + + result=$($cmd_netstat -l \ + | $cmd_sed -n '/tcp/p;/udp/p;/raw/p' \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"proto\": \"%s\", \ + \n\t\t\"recv-q\": %s, \ + \n\t\t\"send-q\": %s, \ + \n\t\t\"localaddr\": \"%s\", \ + \n\t\t\"foreignaddr\": \"%s\", \ + \n\t\t\"state\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Active unix domain sockets +active_unix_socks() { + if hash netstat 2>/dev/null; then active_unix_socks_netstat + else active_unix_socks_ss + fi +} + +# Active unix domain sockets (ss) +active_unix_socks_ss() { + cmd_awk=`which awk` + cmd_ss=`which ss` + cmd_sed=`which sed` + cmd_sort=`which sort` + + result=$($cmd_ss -x \ + | $cmd_sed -e '1d' \ + | $cmd_sort -k6 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"netid\": \"%s\", \ + \n\t\t\"state\": \"%s\", \ + \n\t\t\"recv-q\": %s, \ + \n\t\t\"send-q\": %s, \ + \n\t\t\"local address:port\": \"%s:%s\", \ + \n\t\t\"peer address:port\": \"%s:%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, $8)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Active unix domain sockets (netstat) +active_unix_socks_netstat() { + cmd_awk=`which awk` + cmd_netstat=`which netstat` + cmd_sed=`which sed` + cmd_sort=`which sort` + + result=$($cmd_netstat -l \ + | $cmd_sed -n '/unix/p' \ + | $cmd_sort -k6 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"proto\": \"%s\", \ + \n\t\t\"refcnt\": %s, \ + \n\t\t\"flags\": \"%s%s%s\", \ + \n\t\t\"type\": \"%s\", \ + \n\t\t\"state\": \"%s\", \ + \n\t\t\"i-node\": \"%s\", \ + \n\t\t\"path\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, $8, $9)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# Check is application installed or not +check_app() { + cmd_awk=`which awk` + + result=$(whereis -S /opt -f $1 \ + | $cmd_awk -F: '{if(length($2)==0) { installed="false"; } else { installed="true"; } \ + print \ + "{ \ + \"binary\": \""$1"\", \ + \"location\": \""$2"\", \ + \"installed\": "installed" \ + }"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# SMB processes +smb_proc() { + cmd_awk=`which awk` + cmd_sed=`which sed` + cmd_smbstatus=`which smbstatus` + cmd_sort=`which sort` + + result=$(sudo $cmd_smbstatus -pf \ + | $cmd_sed -e '1,4d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"pid\": %s, \ + \n\t\t\"username\": \"%s\", \ + \n\t\t\"group\": \"%s\", \ + \n\t\t\"machine\": \"%s\", \ + \n\t\t\"protocol ver\": \"%s\", \ + \n\t\t\"encryption\": \"%s\", \ + \n\t\t\"signing\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# SMB shares +smb_shares() { + cmd_awk=`which awk` + cmd_sed=`which sed` + cmd_smbstatus=`which smbstatus` + cmd_sort=`which sort` + + result=$(sudo $cmd_smbstatus -Sf \ + | $cmd_sed -e '1,3d;/^\s*$/d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {print "\t{"} \ + {printf("\t\t\"service\": \"%s\", \ + \n\t\t\"pid\": %s, \ + \n\t\t\"machine\": \"%s\", \ + \n\t\t\"connected at\": \"%s %s %s %s %s %s\", \ + \n\t\t\"encryption\": \"%s\", \ + \n\t\t\"signing\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +# SMB locked files +smb_locks() { + cmd_awk=`which awk` + cmd_sed=`which sed` + cmd_smbstatus=`which smbstatus` + cmd_sort=`which sort` + + result=$(sudo $cmd_smbstatus -Lf \ + | $cmd_sed -e '1,3d;/^\s*$/d' \ + | $cmd_sort -k1 \ + | $cmd_awk 'BEGIN \ + {print "["} \ + {if (NR > 1) print ","} \ + {fname = ""} \ + {ftime = ""} \ + {for (i = 8; i < NF - 4; i++) fname = fname" "$i} \ + {for (i = NF - 4; i < NF; i++) ftime = ftime" "$i} \ + {print "\t{"} \ + {printf("\t\t\"pid\": %s, \ + \n\t\t\"uid\": %s, \ + \n\t\t\"deny mode\": \"%s\", \ + \n\t\t\"access\": \"%s\", \ + \n\t\t\"r/w\": \"%s\", \ + \n\t\t\"oplock\": \"%s\", \ + \n\t\t\"share path\": \"%s\", \ + \n\t\t\"name\": \"%s\", \ + \n\t\t\"time\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, fname, ftime)} \ + END \ + {print "\n\t]"}') + if [ -z "$result" ]; then echo {} + else echo $result | _print_parsed + fi +} + +function_name="$1" +parameter="$2" + +${function_name} ${parameter} diff --git a/system/scripts/json_transmission.php b/system/scripts/json_transmission.php new file mode 100644 index 0000000..70ba6dd --- /dev/null +++ b/system/scripts/json_transmission.php @@ -0,0 +1,15 @@ + 1) print ","}{print "\t{"}{printf("\t\t\"ID\": \"%s\",\n\t\t\"Done\": \"%s\",\n\t\t\"Have\": \"%s %s\",\n\t\t\"ETA\": \"%s\",\n\t\t\"Up\": \"%s\",\n\t\t\"Down\": \"%s\",\n\t\t\"Ratio\": \"%s\",\n\t\t\"Status\": \"%s\",\n\t\t\"Name\": \"%s\"\n\t}", $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)}END{print "\n]"}' \ No newline at end of file diff --git a/system/scripts/smart_temp.sh b/system/scripts/smart_temp.sh new file mode 100644 index 0000000..143cb38 --- /dev/null +++ b/system/scripts/smart_temp.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +cmd_awk=`which awk` +cmd_grep=`which grep` +cmd_hddtemp=`which hddtemp` +cmd_smartctl=`which smartctl` + +drvtemp=$(sudo $cmd_hddtemp $1) +hdd=$(echo "$drvtemp" | $cmd_awk -F: '{print $1}') +model=$(echo "$drvtemp" | $cmd_awk -F: '{print $2}') +result=$(sudo $cmd_smartctl -a $1 | $cmd_grep "Current Drive Temperature" | $cmd_awk '{print $4"°"$5}') + +if [ -z "$result" ]; +then + result=$(echo "$drvtemp" | $cmd_awk -F: '{print $3}') +fi + +echo $hdd: $model: $result diff --git a/system/scripts/trim.awk b/system/scripts/trim.awk new file mode 100644 index 0000000..50c44b6 --- /dev/null +++ b/system/scripts/trim.awk @@ -0,0 +1,3 @@ +function ltrim(s) { sub(/^[ \t\r\n]+/, "", s); return s } +function rtrim(s) { sub(/[ \t\r\n]+$/, "", s); return s } +function trim(s) { return rtrim(ltrim(s)); } \ No newline at end of file diff --git a/system/settings.php b/system/settings.php new file mode 100644 index 0000000..6fb69b5 --- /dev/null +++ b/system/settings.php @@ -0,0 +1,43 @@ + SHUTDOWN_ENABLED, + 'reboot_enabled' => REBOOT_ENABLED, + 'w_transmisson_enabled' => W_TRANSMISSION_ENABLED); + +/* Configure autoloader */ +require_once($cp_root . '/system/classes/CAI/CAICP/src/PSR4Autoloader.php'); + +$autoloader = new \CAI\CAICP\PSR4Autoloader; +$autoloader->register(); +$autoloader->addNamespace('CAI\CAICP', $cp_root . '/system/classes/CAI/CAICP/src'); +//$Autoloader->addNamespace('CAI\CAICP', $cp_root . '/system/classes/CAI/CAICP/tests'); + +/* Create system objects */ +$user_cfg = new \CAI\CAICP\Settings(CP_ROOT_REL); +$lang = new \CAI\CAICP\Localization(CP_ROOT_REL, $user_cfg->get()); diff --git a/system/ui/forms/settings/css/styles.css b/system/ui/forms/settings/css/styles.css new file mode 100644 index 0000000..2dbade6 --- /dev/null +++ b/system/ui/forms/settings/css/styles.css @@ -0,0 +1,79 @@ +/* Common classes block */ +.hidden { +/* visibility: hidden; */ + display: none; +} +.full_width { + width: 100%; +} +.bold { + font-weight: bold; +} + +/* Elements block */ +html, body { + margin: 0; + padding: 0; + min-width: 600px; + height: 100%; + width: 100%; +/* background: url(../gfx/bg.jpg) repeat fixed; */ + font-family: Arial, sans-serif; +} + +hr { + margin: 0; + padding: 0; + border: 0; + height: 1px; + background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); + background: linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.25), rgba(0,0,0,0)); +} + +fieldset { + border: solid 1px #ccc; + margin: 5px 0; +} + +form { + margin: 5px; + transition: all 0.5s ease-out 0.1s; +} + +/* Language selector */ +input, select {border: solid 1px #999;} + +.select_lang { + border : 0; + background-color : transparent; +} +.select_lang option { + color: #000; + background-color: rgba(255, 255, 255, 0.85); +} +/* +select#gender option[value="male"] { background-image:url(male.png); } +select#gender option[value="female"] { background-image:url(female.png); } +select#gender option[value="others"] { background-image:url(others.png); } +*/ +.input_ru { + background-image: url('../gfx/flags/russian-16x16.png'); + background-repeat: no-repeat; + background-attachment: scroll; + background-position: 2px center; + padding-left: 21px; + margin-left: 1px; +} +.input_en { + background-image: url('../gfx/flags/uk-16x16.png'); + background-repeat: no-repeat; + background-attachment: scroll; + background-position: 2px center; + padding-left: 21px; + margin-left: 1px; +} + +#in_submit {margin-top: 5px}; \ No newline at end of file diff --git a/system/ui/forms/settings/index.html b/system/ui/forms/settings/index.html new file mode 100644 index 0000000..7cd7c4a --- /dev/null +++ b/system/ui/forms/settings/index.html @@ -0,0 +1,103 @@ + + + + CAI CP Settings + + + + + +
+ +
+ + + diff --git a/system/ui/forms/settings/js/settings.js b/system/ui/forms/settings/js/settings.js new file mode 100644 index 0000000..9659494 --- /dev/null +++ b/system/ui/forms/settings/js/settings.js @@ -0,0 +1,278 @@ +// MIT License: +// +// Copyright (c) 2016-2017, Alexander I. Chebykin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * CAI CP v.0.9 + * + * @module : Configuration subsystem + * @author : Alexander I. Chebykin + * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin + * @version : 0.9 + * @build date : 2017-07-23 + * @license : MIT + * @link : https://github.com/CAI79/CAI-CP + ******************************************************************/ + +(function () { + 'use strict'; + + /** + * Return GET parameter + * + * @param {string} key Key name + * + * returns {string} + */ + function $_GET(key) { + var s = window.location.search; + + s = s.match(new RegExp(key + '=([^&=]+)')); + + return s ? s[1] : false; + } + + /** + * SetupSystem constructor + * + * @returns {undefined} + */ + function SetupSystem() { + this.locale = 'en'; + this.langs = []; + this.tranlation = []; + } + + /** + * + * @param {int} init_level Initialization level: 1 - configure language + * 2 - do localization + * 3 - load languages list + * + * @returns {undefined} + */ + SetupSystem.prototype.init = function (init_level) { + var setup_instance = this, + request = new XMLHttpRequest(), + res_data; + + switch (init_level) { + case 1: // Configure language + if ($_GET('lang') !== '') { + this.locale = $_GET('lang'); + } else { + request.open('GET', '../../../json/settings.json'); + request.send(); + + request.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + res_data = JSON.parse(this.responseText); + setup_instance.locale = res_data.lang; + } + } + }; + } + this.init(2); + break; + case 2: // Do localization + request.open('GET', '../../../json/locale/' + this.locale + '.json'); + request.send(); + + request.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + res_data = JSON.parse(this.responseText); + setup_instance.tranlation = res_data; + + document.getElementById('lbl_dim_on_create').textContent = res_data.dim_on_create; + document.getElementById('lbl_default_language').textContent = res_data.default_language; + document.getElementById('lbl_check_files_rights').textContent = res_data.check_files_rights; + document.getElementById('lbl_check_hdd_temp_interval').textContent = res_data.check_hdd_temp_int; + document.getElementById('lbl_check_users_online_interval').textContent = res_data.check_users_online_int; + document.getElementById('lbl_max_hdd_temp').textContent = res_data.max_hdd_temp; + document.getElementById('lbl_check_smart_interval').textContent = res_data.check_smart_int; + document.getElementById('lbl_temp_secs').textContent = res_data.secs; + document.getElementById('lbl_smart_secs').textContent = res_data.secs; + document.getElementById('lbl_users_online_secs').textContent = res_data.secs; + document.getElementById('fsl_general').textContent = res_data.general_settings; + document.getElementById('fsl_monitoring').textContent = res_data.monitoring; + document.getElementById('fsl_apps').textContent = res_data.applications; + document.getElementById('lbl_enabled').textContent = res_data.enbld; + document.getElementById('lbl_app_name').textContent = res_data.application; + document.getElementById('lbl_app_ver').textContent = res_data.version; + document.getElementById('lbl_app_author').textContent = res_data.author; + document.getElementById('in_submit').value = res_data.save; + + document.getElementById("frm_submit").classList.remove('hidden'); + } + } + }; + this.init(3); + break; + case 3: // Load languages list + request.open('GET', '../../../json/settings.json'); + request.send(); + + request.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + var select_lang = document.getElementById('default_language'); + + res_data = JSON.parse(this.responseText); + + document.getElementById('dim_on_create').checked = res_data.dim_on_create; + document.getElementById('chk_files_rights').checked = res_data.check_files_rights; + document.getElementById('chk_temp_interval').value = res_data.check_hdd_temp_interval; + document.getElementById('chk_smart_interval').value = res_data.check_smart_interval; + document.getElementById('chk_users_online_interval').value = res_data.check_users_online_interval; + document.getElementById('max_hdd_temp').value = res_data.max_hdd_temp; + + setup_instance.langs = res_data.langs; + + for (var language in res_data.langs) { + var opt_lang = document.createElement('option'); + + opt_lang.value = language; + opt_lang.appendChild(document.createTextNode(res_data.langs[language])); + + if (res_data.lang === language) { + opt_lang.selected = true; + } else if (!$_GET('lang') && setup_instance.locale === language){ + opt_lang.selected = true; + } + + select_lang.appendChild(opt_lang); + } + } + } + }; + this.init(4); + break; + case 4: // Load apps list + request.open('GET', '../../../apps/?do=get_apps'); + request.send(); + + request.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + var apps_table = document.getElementById('tbl_apps'), + apps_tr, + apps_td, + apps_name, + apps_caption, + apps_check, + i = 0; + res_data = JSON.parse(this.responseText); + for (var app_info in res_data) { + apps_tr = document.createElement('tr'); + apps_td = document.createElement('td'); + + apps_caption = document.createElement('input'); + apps_caption.type = 'hidden'; + apps_caption.name = 'app_caption[' + i + ']'; + apps_caption.value = res_data[app_info].caption; + apps_td.appendChild(apps_caption); + + apps_name = document.createElement('input'); + apps_name.type = 'hidden'; + apps_name.name = 'app_name[' + i + ']'; + apps_name.value = app_info; + apps_td.appendChild(apps_name); + + apps_check = document.createElement('input'); + apps_check.type = 'checkbox'; + apps_check.name = 'app_enabled[' + i + ']'; + apps_check.value = '1'; + apps_check.checked = res_data[app_info].enabled; + + apps_td.appendChild(apps_check); + apps_tr.appendChild(apps_td); + apps_td = document.createElement('td'); + apps_td.appendChild(document.createTextNode(res_data[app_info].caption)); + apps_tr.appendChild(apps_td); + apps_td = document.createElement('td'); + apps_td.appendChild(document.createTextNode(res_data[app_info].version)); + apps_tr.appendChild(apps_td); + apps_td = document.createElement('td'); + apps_td.appendChild(document.createTextNode(res_data[app_info].author)); + apps_tr.appendChild(apps_td); + apps_table.appendChild(apps_tr); + i++; + } + } + } + }; + } + }; + + document.addEventListener('DOMContentLoaded', function () { + 'use strict'; + + /** + * Initialize setup object + */ + var setup = new SetupSystem(); + + setup.init(1); + + document.getElementById('frm_submit').onsubmit = function () { + var xhr = new XMLHttpRequest(); + + xhr.onload = function () { + if (xhr.responseText === 'true') { + alert(setup.tranlation.settings_saved); + window.top.location.reload(true); + } else { + alert(setup.tranlation.error); + } + }; + + if (this.method.toLowerCase() === 'post') { + xhr.open (this.method, this.action, true); + xhr.send (new FormData (this)); + } else { + var element, + el_type, + file, + search = ''; + for (var i = 0; i < this.elements.length; i++) { + element = this.elements[i]; + if (!element.hasAttribute('name')) { continue; } + el_type = element.nodeName.toLowerCase() === 'input' ? + element.getAttribute('type').toLowerCase() : 'text'; + if (el_type === 'file') { + for (file = 0; file < element.files.length; + search += '&' + escape(element.name) + '=' + escape(element.files[file++].name)); + } else if ((el_type !== 'radio' && el_type !== 'checkbox') || element.checked) { + search += '&' + escape(element.name) + '=' + escape(element.value); + } + } + xhr.open('get', this.action.replace(/(?:\?.*)?$/, search.replace(/^&/, '?')), true); + xhr.send(null); + } + + return false; + }; + + }); + +}());