diff --git a/BTPanel/__init__.py b/BTPanel/__init__.py index bd9aed84..a0b89d6f 100644 --- a/BTPanel/__init__.py +++ b/BTPanel/__init__.py @@ -42,6 +42,7 @@ if not hooks: # import db dns_client = None app.config['DEBUG'] = os.path.exists('data/debug.pl') +app.config['SSL'] = os.path.exists('data/ssl.pl') #设置BasicAuth basic_auth_conf = 'config/basic_auth.json' @@ -65,6 +66,13 @@ app.config['SESSION_USE_SIGNER'] = True app.config['SESSION_KEY_PREFIX'] = 'BT_:' app.config['SESSION_COOKIE_NAME'] = public.md5(app.secret_key) app.config['PERMANENT_SESSION_LIFETIME'] = 86400 * 30 +if app.config['SSL']: + app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' + app.config['SESSION_COOKIE_SECURE'] = True +else: + app.config['SESSION_COOKIE_SAMESITE'] = None + + Session(app) import common @@ -212,6 +220,23 @@ def error_404(e): } return Response(errorStr,status=404,headers=headers) + +# Flask 403页面勾子 +@app.errorhandler(403) +def error_403(e): + errorStr = ''' +403 Forbidden + +

403 Forbidden

+
nginx
+ +''' + headers = { + "Content-Type": "text/html" + } + return Response(errorStr, status=403, headers=headers) + + # Flask 500页面勾子 @app.errorhandler(500) def error_500(e): @@ -219,9 +244,6 @@ def error_500(e): During handling of the above exception, another exception occurred:''' error_info = public.get_error_info().strip().split(ss)[-1].strip() - if error_info.find("`GLIBC_2.14'") != -1: - public.downloadFile('https://download.bt.cn/auth/libAuth_gcc_4.4.7_x{}.so'.format(public.get_sysbit()),'/www/server/panel/class/libAuth.x86-64.so') - error_info += "\n已尝试自动修复此问题,请刷新页面重试!" request_info = '''REQUEST_DATE: {request_date} PAN_VERSION: {panel_version} OS_VERSION: {os_version} @@ -235,7 +257,7 @@ REQUEST_FORM: {request_form} full_path = request.full_path, request_form = request.form.to_dict(), user_agent = request.headers.get('User-Agent'), - panel_version = public.get_panel_version(), + panel_version = public.version(), os_version = public.get_os_version() ) @@ -294,7 +316,7 @@ def site(pdata = None): import panelSite siteObject = panelSite.panelSite() - defs = ('upload_csv','create_website_multiple','del_redirect_multiple','del_proxy_multiple','delete_dir_auth_multiple', + defs = ('check_del_data','upload_csv','create_website_multiple','del_redirect_multiple','del_proxy_multiple','delete_dir_auth_multiple', 'delete_dir_bind_multiple','delete_domain_multiple','set_site_etime_multiple','set_site_php_version_multiple', 'delete_website_multiple','set_site_status_multiple','get_site_err_log','get_site_domains','GetRedirectFile', 'SaveRedirectFile','DeleteRedirect','GetRedirectList','CreateRedirect','ModifyRedirect', @@ -348,7 +370,7 @@ def database(pdata = None): return render_template('database.html',data=data) import database databaseObject = database.database() - defs = ('get_mysql_user','check_mysql_ssl_status','write_ssl_to_mysql','GetdataInfo','GetInfo','ReTable','OpTable','AlTable','GetSlowLogs','GetRunStatus', + defs = ('check_del_data','get_database_size', 'get_mysql_user','check_mysql_ssl_status','write_ssl_to_mysql','GetdataInfo','GetInfo','ReTable','OpTable','AlTable','GetSlowLogs','GetRunStatus', 'SetDbConf','GetDbStatus','BinLog','GetErrorLog','GetMySQLInfo','SetDataDir','SetMySQLPort', 'AddDatabase','DeleteDatabase','SetupPassword','ResDatabasePassword','ToBackup','DelBackup', 'InputSql','SyncToDatabases','SyncGetDatabases','GetDatabaseAccess','SetDatabaseAccess') @@ -496,7 +518,7 @@ def ssh_security(pdata=None): firewallObject = ssh_security.ssh_security() defs = ('san_ssh_security', 'set_password', 'set_sshkey', 'stop_key', 'get_config', 'stop_password', 'get_key', 'return_ip', 'add_return_ip', 'del_return_ip', 'start_jian', 'stop_jian', - 'get_jian', 'get_logs','set_root','stop_root','start_auth_method','stop_auth_method','get_auth_method','check_so_file','get_so_file') + 'get_jian', 'get_logs','set_root','stop_root','start_auth_method','stop_auth_method','get_auth_method','check_so_file','get_so_file','get_pin') return publicObject(firewallObject, defs, None, pdata) @@ -599,7 +621,8 @@ def files(pdata = None): return render_template('files.html',data=data) import files filesObject = files.files() - defs = ('get_file_attribute','get_file_hash','CreateLink','get_progress','restore_website','fix_permissions','get_all_back', + defs = ('files_search','files_replace','get_replace_logs', + 'get_file_attribute','get_file_hash','CreateLink','get_progress','restore_website','fix_permissions','get_all_back', 'restore_path_permissions','del_path_premissions','get_path_premissions','back_path_permissions', 'CheckExistsFiles','GetExecLog','GetSearch','ExecShell','GetExecShellMsg','exec_git','exec_composer','create_download_url', 'UploadFile','GetDir','CreateFile','CreateDir','DeleteDir','DeleteFile','get_download_url_list','remove_download_url','modify_download_url', @@ -675,7 +698,7 @@ def config(pdata = None): return render_template( 'config.html',data=data) import config defs = ( - 'set_empty','set_backup_notification','get_panel_ssl_status','set_file_deny', 'del_file_deny', 'get_file_deny', + 'send_by_telegram','set_empty','set_backup_notification','get_panel_ssl_status','set_file_deny', 'del_file_deny', 'get_file_deny', 'get_httpd_access_log_format_parameter','set_httpd_format_log_to_website','get_httpd_access_log_format', 'del_httpd_access_log_format','add_httpd_access_log_format','get_nginx_access_log_format_parameter', 'set_format_log_to_website','get_nginx_access_log_format','del_nginx_access_log_format', @@ -691,7 +714,7 @@ def config(pdata = None): 'get_php_config','get_config','SavePanelSSL','GetPanelSSL','GetPHPConf','SetPHPConf', 'GetPanelList','AddPanelInfo','SetPanelInfo','DelPanelInfo','ClickPanelInfo','SetPanelSSL', 'SetTemplates','Set502','setPassword','setUsername','setPanel','setPathInfo','setPHPMaxSize', - 'getFpmConfig','setFpmConfig','setPHPMaxTime','syncDate','setPHPDisable','SetControl', + 'getFpmConfig','setFpmConfig','setPHPMaxTime','syncDate','setPHPDisable','SetControl','get_settings2','del_tg_info','set_tg_bot', 'ClosePanel','AutoUpdatePanel','SetPanelLock','return_mail_list','del_mail_list','add_mail_address','user_mail_send','get_user_mail','set_dingding','get_dingding', 'get_settings','user_stmp_mail_send','user_dingding_send','get_login_send','set_login_send','clear_login_send','get_login_log','login_ipwhite' ) @@ -708,7 +731,7 @@ def ajax(pdata = None): 'check_user_auth','to_not_beta','get_beta_logs','apple_beta','GetApacheStatus','GetCloudHtml', 'get_load_average','GetOpeLogs','GetFpmLogs','GetFpmSlowLogs','SetMemcachedCache','GetMemcachedStatus', 'GetRedisStatus','GetWarning','SetWarning','CheckLogin','GetSpeed','GetAd','phpSort','ToPunycode', - 'GetBetaStatus','SetBeta','setPHPMyAdmin','delClose','KillProcess','GetPHPInfo','GetQiniuFileList', + 'GetBetaStatus','SetBeta','setPHPMyAdmin','delClose','KillProcess','GetPHPInfo','GetQiniuFileList','get_process_tops','get_process_cpu_high', 'UninstallLib','InstallLib','SetQiniuAS','GetQiniuAS','GetLibList','GetProcessList','GetNetWorkList', 'GetNginxStatus','GetPHPStatus','GetTaskCount','GetSoftList','GetNetWorkIo','GetDiskIo','GetCpuIo', 'CheckInstalled','UpdatePanel','GetInstalled','GetPHPConfig','SetPHPConfig') @@ -777,8 +800,8 @@ def task(pdata = None): if comReturn: return comReturn import panelTask toObject = panelTask.bt_task() - defs = ('get_task_lists','remove_task','get_task_find') - result = publicObject(toObject,defs,None,pdata) + defs = ('get_task_lists', 'remove_task', 'get_task_find', "get_task_log_by_id") + result = publicObject(toObject, defs, None, pdata) return result @app.route('/plugin',methods=method_all) @@ -853,7 +876,7 @@ def download(): attachment_filename=os.path.basename(filename), cache_timeout=0) -@app.route('/cloud',methods=method_get) +@app.route('/cloud',methods=method_all) def panel_cloud(): #从对像存储下载备份文件接口 comReturn = comm.local() @@ -877,7 +900,36 @@ def panel_cloud(): if plugin_name == 'ftp': if download_url.find("ftp") != 0:download_url = "ftp://" + download_url else: - if download_url.find('http') != 0:download_url = 'http://' + download_url + if download_url.find('http') != 0: download_url = 'http://' + download_url + + if "toserver" in get and get.toserver=="true": + download_dir = "/tmp/" + if "download_dir" in get: + download_dir = get.download_dir + local_file = os.path.join(download_dir, get.name) + + input_from_local = False + if "input_from_local" in get: + input_from_local = True if get.input_from_local == "true" else False + + if input_from_local: + if os.path.isfile(local_file): + return { + "status": True, + "msg": "The file already exists and will be restored locally.", + "task_id": -1, + "local_file": local_file + } + from panelTask import bt_task + task_obj = bt_task() + task_id = task_obj.create_task('Download file', 1, download_url, local_file) + return { + "status": True, + "msg": "The download task was created successfully", + "local_file": local_file, + "task_id": task_id + } + return redirect(download_url) #======================普通路由区============================# @@ -951,6 +1003,7 @@ def login(): os.remove(sess_file) except: pass + g.dologin = True return redirect(login_path) if is_auth_path: @@ -1166,20 +1219,23 @@ def panel_public(): @app.route('/favicon.ico',methods=method_get) def send_favicon(): - #图标 + # 图标 + comReturn = comm.local() + if comReturn: return abort(404) s_file = '/www/server/panel/BTPanel/static/favicon.ico' if not os.path.exists(s_file): return abort(404) return send_file(s_file,conditional=True,add_etags=True) -@app.route('/service_status',methods = method_get) -def service_status(): - #检查面板当前状态 - try: - if not 'login' in session: session.clear() - except: - pass - return 'True' +# @app.route('/service_status', methods=method_get) +# def service_status(): +# # 检查面板当前状态 +# try: +# if not 'login' in session: session.clear() +# except: +# pass +# return 'True' + @app.route('/coll',methods=method_all) @app.route('/coll/',methods=method_all) @@ -1209,13 +1265,15 @@ def panel_other(name=None,fun = None,stype=None): if len(tmp) == 1: tmp.append('') stype = tmp[1] - if not name: name = 'coll' if not public.path_safe_check("%s/%s/%s" % (name,fun,stype)): return abort(404) if name.find('./') != -1 or not re.match(r"^[\w-]+$",name): return abort(404) if not name: return public.returnJson(False,'PLUGIN_INPUT_ERR'),json_header p_path = os.path.join('/www/server/panel/plugin/', name) - if not os.path.exists(p_path): return abort(404) + if not os.path.exists(p_path): + if name == 'btwaf' and fun == 'index': + return render_template('error3.html',data={}) + return abort(404) #是否响插件应静态文件 if fun == 'static': @@ -1295,9 +1353,7 @@ def panel_other(name=None,fun = None,stype=None): return public.returnJson(False,public.getMsg('PUBLIC_ERR_RETURN').format(r_type)),json_header return data except: - error_info = public.get_error_info() - public.submit_error(error_info) - return error_info.replace('\n','
\n') + return public.get_error_object(None, plugin_name=name) @app.route('/hook',methods=method_all) @@ -1660,7 +1716,16 @@ def is_login(result): result = make_response(result) request_token = public.GetRandomString(48) session['request_token'] = request_token - result.set_cookie('request_token',request_token,max_age=86400*30) + samesite = app.config['SESSION_COOKIE_SAMESITE'] + secure = app.config['SESSION_COOKIE_SECURE'] + if app.config['SSL'] and request.full_path.find('/login?tmp_token=') == 0: + samesite = 'None' + secure = True + result.set_cookie('request_token', request_token, + max_age=86400 * 30, + samesite= samesite, + secure=secure + ) return result # js随机数模板使用,用于不更新版本号时更新前端文件不需要用户强制刷新浏览器 @@ -1787,7 +1852,7 @@ def ws_panel_thread(get): check_str = '{}{}'.format(get.mod_name, get.def_name) if not re.match("^\w+$", check_str) or get.mod_name in ['public', 'common', 'db', 'db_mysql', 'downloadFile', 'jobs']: - get._ws.send(public.getJson(public.return_status_code(1000, '不安全的mod_name,def_name参数内容'))) + get._ws.send(public.getJson(public.return_status_code(1000, 'Unsafe mod_name, def_name parameter content'))) return if not hasattr(get, 'args'): get._ws.send(public.getJson(public.return_status_code(1001, 'args'))) @@ -1795,21 +1860,21 @@ def ws_panel_thread(get): mod_file = '{}/{}.py'.format(public.get_class_path(), get.mod_name) if not os.path.exists(mod_file): - get._ws.send(public.getJson(public.return_status_code(1000, '指定模块{}不存在'.format(get.mod_name)))) + get._ws.send(public.getJson(public.return_status_code(1000, 'Specified module {} does not exist'.format(get.mod_name)))) return _obj = public.get_script_object(mod_file) if not _obj: - get._ws.send(public.getJson(public.return_status_code(1000, '指定模块{}不存在'.format(get.mod_name)))) + get._ws.send(public.getJson(public.return_status_code(1000, 'Specified module {} does not exist'.format(get.mod_name)))) return _cls = getattr(_obj, get.mod_name) if not _cls: get._ws.send( - public.getJson(public.return_status_code(1000, '在{}模块中没有找到{}对像'.format(get.mod_name, get.mod_name)))) + public.getJson(public.return_status_code(1000, 'The {} object was not found in the {} module'.format(get.mod_name, get.mod_name)))) return _def = getattr(_cls(), get.def_name) if not _def: get._ws.send( - public.getJson(public.return_status_code(1000, '在{}对像中没有找到{}方法'.format(get.mod_name, get.def_name)))) + public.getJson(public.return_status_code(1000, 'The {} object was not found in the {} module'.format(get.mod_name, get.def_name)))) return result = { 'callback': get.ws_callback, @@ -1931,7 +1996,9 @@ def sock_recv(cmdstring, ws): sock_pids[p.pid] = ws kill_closed() while p.poll() is None: - ws.send(p.stdout.readline().decode()) + send_line = p.stdout.readline().decode() + if not send_line or send_line.find('tail: ') != -1: continue + ws.send(send_line) ws.send(p.stdout.read().decode()) except: kill_closed() @@ -1954,10 +2021,10 @@ def close_sock_shell(): skey = public.md5(cmdstring) pid = cache.get(skey) if not pid: - return json.dumps(public.return_data(False, [], error_msg='指定sock已终止!')), json_header + return json.dumps(public.return_data(False, [], error_msg='The specified sock has been terminated!')), json_header os.kill(pid, 9) cache.delete(skey) - return json.dumps(public.return_data(True, '操作成功!')), json_header + return json.dumps(public.return_data(True, 'Successful operation!')), json_header def check_csrf_websocket(ws,args): ''' diff --git a/BTPanel/static/css/site.css b/BTPanel/static/css/site.css index 5a147f30..a29e5720 100644 --- a/BTPanel/static/css/site.css +++ b/BTPanel/static/css/site.css @@ -249,6 +249,10 @@ html { padding-bottom: 70px } +.mt5 { + margin-top: 5px +} + .mt10 { margin-top: 10px } @@ -3129,6 +3133,15 @@ html .menu .menu_exit:hover { height: 40px; box-sizing: border-box; } +.databaseImportTable>tbody>tr>td { + vertical-align: middle; + padding: 5px 10px; + height: 40px; + box-sizing: border-box; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} .table>tbody{ border-bottom: 1px solid #ddd; } @@ -9360,7 +9373,6 @@ select[disabled]{ } .redirect_check label span, .redirect_checkbox label span, -.ssl_checkbox label span, .redirect_tourl label span{ max-width: 55px; display: inline-block; @@ -9392,6 +9404,9 @@ select[disabled]{ .redirect_tourl .radio_item input{ margin-right: 5px; } +.site_ssl_check i.form-checkbox { + vertical-align: inherit; +} /*首页概览网卡选择*/ @@ -9498,7 +9513,8 @@ select[name="network-io"]{ width: 100%; position: relative; } -.bt_table .cust—checkbox{ +.bt_table .cust—checkbox, +.bt-form .form-checkbox{ display: inline-block; width: 16px; height: 16px; @@ -9510,11 +9526,14 @@ select[name="network-io"]{ border-radius: 2px; } .bt_table .cust—checkbox.active, -.bt_table .cust—checkbox.selected{ +.bt_table .cust—checkbox.selected, +.bt-form .form-checkbox.active, +.bt-form .form-checkbox.selected{ background-color: #20a53a; border-color: #20a53a; } -.bt_table .cust—checkbox.active::after{ +.bt_table .cust—checkbox.active::after, +.bt-form .form-checkbox.active::after{ content: ''; position: absolute; display: block; @@ -9528,7 +9547,8 @@ select[name="network-io"]{ border-width: 0 2px 2px 0; transform: rotate(45deg); } -.bt_table .cust—checkbox.selected::after{ +.bt_table .cust—checkbox.selected::after, +.bt-form .form-checkbox.selected::after{ content: ''; position: absolute; display: block; @@ -9540,15 +9560,23 @@ select[name="network-io"]{ width: 10px; background-color: #fff; } -.bt_table .cust—checkbox+input{ +.bt_table .cust—checkbox+input, +.bt-form .form-checkbox+input{ display: none; } -.bt_table .cursor-pointer{ +.bt_table .cursor-pointer, +.bt-form .cursor-pointer{ cursor: pointer; } .bt_table thead .sort-active span{ font-weight: 500; } +.vertical_middle{ + vertical-align: middle; +} +.bt-form .form-checkbox-label span{ + font-weight: 400; +} /* .bt_table thead .sort-active .glyphicon{ color: #20a53a; } */ @@ -9606,7 +9634,7 @@ select[name="network-io"]{ outline: none; padding-left: 8px; vertical-align: top; - width:230px; + width:240px; } .tootls_group .bt_search .search_input:focus{ border-color: #20a53a; @@ -9735,15 +9763,19 @@ select[name="network-io"]{ border-radius: 2px; height: 28px; line-height: 28px; - vertical-align: top; cursor: pointer; color:#666; position: relative; } +.bt_table_select_group { + vertical-align: top; +} + .bt_select_updown{ - height: 30px; + height: 30px; line-height: 30px; + vertical-align: middle; } .bt_select_updown .bt_select_value{ height: 28px; @@ -9751,6 +9783,9 @@ select[name="network-io"]{ padding:0 10px; width: 100%; position: relative; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .bt_select_updown .bt_select_list{ display: none; @@ -9776,6 +9811,9 @@ select[name="network-io"]{ line-height: 28px; color: #333; padding:0 10px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .bt_select_updown .bt_select_list .item.disabled{ opacity: .65; @@ -9957,6 +9995,174 @@ select[name="network-io"]{ } /*批量创建站点end*/ + +/*站点删除安全验证*/ + +.delete_site_layer #site_delete_form,.verify_site_layer_info .check_delete_site_main { + padding: 30px 25px !important; +} + +.delete_site_layer .check_title{ + font-size: 16px; + padding-left: 55px; + font-weight: bold; + color: #111; +} +.delete_site_layer .check_type_group{ + padding:20px 0 20px 55px; +} +.delete_site_layer .layui-layer-content .layui-layer-ico{ + left: 40px !important; + top: 27px !important; + position: absolute; + width: 30px; + height: 30px; +} +.delete_site_layer .check_type_group label{ + height:24px; + line-height:24px; + cursor:pointer; + margin-right:35px; + margin-bottom:0; +} +.delete_site_layer .check_type_group label span{ + display:inline-block; + font-weight:normal; + vertical-align:middle; + margin:0 0 0 5px; + position:relative; + top: 0; +} +.delete_site_layer .check_type_group input{ + height:15px; + width:15px; + vertical-align:middle; + margin:0; + cursor:pointer; +} +.delete_site_layer .webDelete .vcode{ + margin: 0 15px 5px 15px; + padding-left: 40px; +} +.delete_site_layer .webDelete .vcode input{ + width: 80px !important; + border: 1px solid #ccc; + padding-left: 5px; + outline: none; +} +.delete_site_layer .webDelete .vcode input:focus, +.delete_site_layer .webDelete .vcode input:active{ + border-color:#20a53a !important; +} + +/*END*//*验证站点删除二次信息*/ + + +.verify_site_layer_info .layui-layer-btn0{ + background-color: #e00000 !important; +} +.verify_site_layer_info .layui-layer-btn0:hover{ + background-color: #ff0000 !important; +} +.verify_site_layer_info.active .layui-layer-btn0{ + background: #e000008c !important; + cursor: no-drop; +} +.check_delete_site_main { + height:100%; + padding: 10px 15px; +} +.check_delete_site_main .layui-layer-ico{ + left: 30px !important; + top: 28px !important; + position: absolute; + width: 30px; + height: 30px; +} +.check_layer_title{ + font-size:19px; + font-weight:bold; + padding-left: 50px; +} +.check_layer_content{ + margin: 25px 0; + border: 1px solid #ececec; + overflow-y: auto; + height: 300px; +} + +.check_layer_item{ + padding: 12px; + max-height: 80px; + border-radius: 2px; + border-bottom: 1px solid #ececec; +} +.check_layer_item:last-child{ + border-bottom:none; +} +.check_layer_item:first-child{ + border-bottom: 1px solid #ececec; +} +.check_layer_item:hover{ + background: #fcfcfc; +} +.check_layer_content .check_layer_site span, +.check_layer_content .check_layer_database span{ + display:inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.check_layer_content span i.warning{ + color: #EE3838; + font-weight: bold; +} +.check_layer_content span i{ + font-style: inherit; +} + +.check_layer_content .check_layer_site, +.check_layer_content .check_layer_database{ + display:inline-block; + font-size:13px; + height:25px; + line-height:25px; + vertical-align:middle; +} + +.check_layer_content .check_layer_site span:nth-child(1), +.check_layer_content .check_layer_database span:nth-child(1){ + width:180px; +} +.check_layer_content .check_layer_site span:nth-child(2), +.check_layer_content .check_layer_database span:nth-child(2){ + width: 310px; + line-height:26px; + height:25px; +} + +.check_layer_content .check_layer_database span:nth-child(2) .glyphicon{ + line-height: 15px; +} +.check_layer_content .check_layer_site span:nth-child(3), +.check_layer_content .check_layer_database span:nth-child(3){ + width:150px; +} + +.check_layer_error{ + color:red; + font-size: 13px; + margin-bottom: 5px; +} +.check_layer_message{ + font-size: 13px; + color: #333; + margin-top: 5px; +} +/*end*/ + + .custom_layer .layui-layer-content .tab-body .tab-con{ max-height: 505px; overflow: auto; @@ -10368,4 +10574,369 @@ background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODIiIGhlaWdodD0iODIiIHh } .pro_introduce_content .item span:nth-child(2){ padding-left: 14px; -} \ No newline at end of file +} + +#bt_database_table .tootls_group .pull-left button:nth-child(4):after{ + content: ' '; + position: relative; + left: -76px; + border-left: 1px solid #ececec; +} +/*node项目管理*/ +.node_project_dialog .mask_module{ + position: absolute; + height: 100%; + background-color: rgba(0,0,0,0.5); + top: 0; + width: 610px; + left: 170px; + z-index: 99; +} +.node_project_dialog .mask_module .node_mask_module_text{ + background: #fff; + /*color: #000;*/ + width:280px; + text-align: center; + position: absolute; + padding: 15px 30px; + left: 36%; + top: 43%; + margin-left: -140px; + border: 1px solid #efefef; + display: inline-block; + border-radius:2px; +} +.node_project_dialog .node_project_pid_datail h3.tname { + margin:10px 10px 10px 0; + color: #999; + font-size: 12px; + font-weight: bold; +} +#node_module_list .divtable, +#project_domian_list .divtable{ + border: 1px solid #ddd; + word-break: break-all; +} +#node_module_list .table, +#project_domian_list .table{ + border:none; +} +/*node项目管理 end*/ + + +/*文件内容查找替换*/ + /*查找textarea样式*/ +.replace_content_view .replace_content_line:first-child{ + position: relative; + height: 36px; +} +.replace_content_view .replace_content_line:first-child textarea{ + width: 485px; + border: 1px solid #ccc; + border-right: none; + height: 36px; + position: absolute; + padding: 10px 0 0 10px; +} +.replace_content_view .replace_content_line:first-child i.history_search{ + position: absolute; + right: 85px; + top: 1px; + cursor: pointer; + line-height: 34px; + padding: 0 4px; +} +.replace_content_view .replace_content_line:first-child i.history_search:hover{ + background-color: #f0f0f1; + border-left: 1px solid #e2e2e2; +} +.replace_content_view .replace_content_line:first-child .normalBtnStyle{ + position: absolute; + right: 0; + vertical-align: top; +} + /*查找textarea样式 end*/ + /*历史搜索列表*/ +.replace_content_view .history_search_list { + position: absolute; + top: 36px; + left: 10px; + background-color: #fff; + width: 485px; + font-size: 14px; + z-index: 10; + cursor: pointer; + border: 1px solid #ececec; + box-shadow: 4px 2px 5px 0 #ececec; +} +.replace_content_view .history_search_list li{ + border-bottom: 1px solid #ececec; + padding: 5px 10px; +} +.replace_content_view .history_search_list li:hover{ + background: #f0f0f0; + color: #555; +} + /*历史搜索列表 end*/ + +.replace_content_view .checkbox_config { + color: #666; + border-top: none; + height: 26px; + line-height: 26px; + display: inline-block; + cursor: pointer; +} +.replace_content_view .checkbox_config:nth-child(2n){ + margin-left: 10px; +} +.replace_content_view .checkbox_config .laberText { + font-weight: normal; + font-size: 13px; +} + +.replace_content_view .file_path_switch { + position: absolute; + right: 5px; + color: #666; + width: 65px; + line-height: 31px; + top: 2px; + cursor: pointer; +} +.replace_content_view .checkbox_config .file_find_radio, +.replace_content_view .checkbox_config .file_find_checkbox, +.replace_content_view .file_path_switch .file_find_checkbox { + display: inline-block; + width: 16px; + height: 16px; + border: 1px solid #ccc; + background-color: #fff; + vertical-align: sub; + position: relative; + border-radius: 2px; + margin-right: 4px; +} +.replace_content_view .checkbox_config .file_find_radio.active, +.replace_content_view .checkbox_config .file_find_checkbox.active, +.replace_content_view .file_path_switch .file_find_checkbox.active { + background-color: #20a53a; + border-color: #20a53a; +} + +.replace_content_view .checkbox_config .file_find_radio.active::after, +.replace_content_view .checkbox_config .file_find_checkbox.active::after, +.replace_content_view .file_path_switch .file_find_checkbox.active::after { + content: ''; + position: absolute; + display: block; + left: 50%; + top: 50%; + margin-left: -2.5px; + margin-top: -6px; + width: 5px; + height: 10px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.replace_content_view .checkbox_config .file_find_radio{ + border-radius: 20px; +} +.replace_content_view .replace_content_box .glyphicon-folder-open{ + position: absolute; + right: 15px; + top: 145px; +} +.replace_content_view .file_path_switch .laberText { + position: absolute; + right: 5px; + top: 0; + font-weight: 400; +} +.replace_content_view .bt-input-text { + height: 36px; + line-height: 36px; + padding-left: 10px; +} +.replace_content_view .replace_content_line { + margin-bottom: 20px; + } +.replace_content_view .replace_content_line .tname { + display: block; + float: left; + height: 36px; + line-height: 36px; + width: 50px; + font-size: 14px; + margin-right: 10px; +} +.replace_content_view .replace_content_line .info-r { + margin-left: 50px; + position: relative; +} +/*按钮样式*/ +.replace_content_view .normalBtnStyle { + min-width: 85px; + height: 36px; +} +.replace_content_view .normalBtnStyle:hover{ + background-color:#20a53a; + color: #fff; +} +.replace_content_view .checkBtn { + border: none; + background-color: #20a53a; + color: #fff; +} +.replace_content_view .replaceBtn { + color: #20a53a; + background-color: #fff; + border: 1px solid #20a53a; + border-radius: 2px; +} +.replace_content_view .matchModel { + width: 570px; + height: 35px; + display: flex; + align-items: center; + justify-content: space-between; +} +/*匹配容器*/ +.replace_content_view .match_container .header { + display: flex; + justify-content: space-between; + border-bottom: 1px solid #d4d4d4; + padding-bottom: 5px; +} +.replace_content_view .match_container .main { + overflow: auto; + height: 290px; + margin-top: 20px; +} +.replace_content_view .match_result_file_content{ + overflow: auto; + word-wrap: break-word; + word-break: normal; + border-top: 1px solid #eee; + display: none; +} +.replace_content_view .match_result_file_content .match_result_detail{ + font-size: 13px; + padding: 5px 5px 5px 15px; +} +.replace_content_view .match_result_file_content .match_result_detail:hover{ + background-color: #eee; +} +.replace_content_view .match_result_file_content.matchShow{ + display: block; +} +.replace_content_view .match_result_file_content .match_result_detail i{ + color: red; + font-style: inherit; + background-color: aliceblue; +} +.replace_content_view .match_content_item { + border: 1px solid #eee; + border-radius: 3px; + margin-bottom: 15px; +} +.replace_content_view .match_content_title{ + background-color: #fbfbfb; + padding: 5px 15px 5px 5px; +} +.replace_content_view .match_result_file_title{ + cursor: pointer; + width: 490px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; +} +.replace_content_view .match_content_title i.glyphicon{ + margin-right: 10px; + margin-left:5px; +} +/*文件内容查找替换 end*/ +/*删除数据库弹窗样式*/ +.promptDelete p { + font-size: 14px; + } + + .promptDelete .confirm-info-box { + position: relative; + margin-top: 15px; + } + + .promptDelete input { + width: 100%; + height: 30px; + padding-left: 5px; + } + + .promptDelete .placeholder { + line-height: 24px; + left: 7px; + top: 2px; + } + +/*删除数据库弹窗样式 end*/ +/*站点删除安全验证*/ +.delete_site_layer #site_delete_form,.verify_site_layer_info .check_delete_site_main { + padding: 30px 25px !important; +} + +.delete_site_layer .check_title{ + font-size: 16px; + padding-left: 55px; + font-weight: bold; + color: #111; +} +.delete_site_layer .check_type_group{ + padding:20px 0 20px 55px; +} +.delete_site_layer .layui-layer-content .layui-layer-ico{ + left: 40px !important; + top: 27px !important; + position: absolute; + width: 30px; + height: 30px; +} +.delete_site_layer .check_type_group label{ + height:24px; + line-height:24px; + cursor:pointer; + margin-right:35px; + margin-bottom:0; +} +.delete_site_layer .check_type_group label span{ + display:inline-block; + font-weight:normal; + vertical-align:middle; + margin:0 0 0 5px; + position:relative; + top: 0; +} +.delete_site_layer .check_type_group input{ + height:15px; + width:15px; + vertical-align:middle; + margin:0; + cursor:pointer; +} +.delete_site_layer .webDelete .vcode{ + margin: 0 15px 5px 15px; + padding-left: 40px; +} +.delete_site_layer .webDelete .vcode input{ + width: 80px !important; + border: 1px solid #ccc; + padding-left: 5px; + outline: none; +} +.delete_site_layer .webDelete .vcode input:focus, +.delete_site_layer .webDelete .vcode input:active{ + border-color:#20a53a !important; +} +/*END*/ \ No newline at end of file diff --git a/BTPanel/static/js/config.js b/BTPanel/static/js/config.js index a374e32a..5ef98aaa 100644 --- a/BTPanel/static/js/config.js +++ b/BTPanel/static/js/config.js @@ -235,7 +235,7 @@ var three_channel_status = {}; }); get_three_channel(function(res){ three_channel_status = res; - $('#channel_auth').val(!res.user_mail.user_name && !res.dingding.dingding ? 'Email is not set':(res.user_mail.user_name? 'Email is set':(res.dingding.dingding? 'dingding is set': ''))) + $('#channel_auth').val(!res.user_mail.user_name && !res.telegram.setup ? 'Email is not set':(res.user_mail.user_name? 'Email is set':(res.telegram.setup? 'Telegram is set': ''))) }); get_login_send(function(rdata){ $('#panel_report').val(!rdata.status ? 'Email is not set':(rdata.msg.mail? 'Already set':'Not set')) @@ -243,7 +243,7 @@ var three_channel_status = {}; })() function get_three_channel(callback){ - $.post('/config?action=get_settings',function(res){ + $.post('/config?action=get_settings2',function(res){ if(callback) callback(res); }); } @@ -1142,277 +1142,6 @@ function modify_basic_auth() { } }); } -function open_three_channel_auth(){ - get_channel_settings(function(rdata){ - var isOpen = rdata.dingding.info.msg.isAtAll == 'True' ? 'checked': ''; - var isDing = rdata.dingding.info.msg == 'No information'? '': rdata.dingding.info.msg.dingding_url; - layer.open({ - type: 1, - area: "600px", - title: "Setting up notification", - closeBtn: 2, - shift: 5, - shadeClose: false, - content: '
\ -
\ -
\ -

Email

\ -
\ -
\ -
\ -
\ -
\ -
\ - \ - \ -
\ -
\ -
\ -
EmailOperating
\ -
\ -
\ -
\ -
\ -
\ - \ -
\ -
\ -
\ -
' - }) - $(".bt-w-menu p").click(function () { - var index = $(this).index(); - $(this).addClass('bgw').siblings().removeClass('bgw'); - $('.conter_box').eq(index).show().siblings().hide(); - }); - get_receive_list(); - }) -} -function sender_info_edit(){ - var loadT = layer.msg('Getting profile, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - $.post('/config?action=get_settings',function(rdata){ - layer.close(loadT); - var qq_mail = rdata.user_mail.info.msg.qq_mail == undefined ? '' : rdata.user_mail.info.msg.qq_mail, - qq_stmp_pwd = rdata.user_mail.info.msg.qq_stmp_pwd == undefined? '' : rdata.user_mail.info.msg.qq_stmp_pwd, - hosts = rdata.user_mail.info.msg.hosts == undefined? '' : rdata.user_mail.info.msg.hosts, - port = rdata.user_mail.info.msg.port == undefined? '' : rdata.user_mail.info.msg.port; - layer.open({ - type: 1, - area: "485px", - title: "Set sender email information", - closeBtn: 2, - shift: 5, - shadeClose: false, - content: '
\ -
\ - Sender email\ -
\ - \ -
\ -
\ -
\ - smtp password\ -
\ - \ -
\ -
\ -
\ - smtp server\ -
\ - \ -
\ -
\ -
\ - smtp port\ -
\ - \ - \ -
\ -
\ - \ -
\ - \ -
\ -
', - success:function(layers,index){ - var _option = ''; - if(select_port(port)){ - if(port == '465' || port == ''){ - _option = '' - }else if(port == '25'){ - _option = '' - }else{ - _option = '' - } - }else{ - _option = '' - } - $("#port_select").html(_option) - $("#port_select").change(function(e){ - if(e.target.value == 'other'){ - $("#port_select").css("width","100px"); - $('input[name=channel_email_port]').css("display","inline-block"); - }else{ - $("#port_select").css("width","300px"); - $('input[name=channel_email_port]').css("display","none"); - } - }) - $(".SetChannelEmail").click(function(){ - var _email = $('input[name=channel_email_value]').val(); - var _passW = $('input[name=channel_email_password]').val(); - var _server = $('input[name=channel_email_server]').val(); - if($('#port_select').val() == 'other'){ - _port = $('input[name=channel_email_port]').val(); - }else{ - _port = $('#port_select').val() - } - if(_email == ''){ - return layer.msg('Email address cannot be empty!',{icon:2}); - }else if(_passW == ''){ - return layer.msg('STMP password cannot be empty!',{icon:2}); - }else if(_server == ''){ - return layer.msg('STMP server address cannot be empty!',{icon:2}); - }else if(_port == ''){ - return layer.msg('STMP server port cannot be empty!',{icon:2}); - } - var loadT = layer.msg('The notification is being generated, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - layer.close(index) - $.post('/config?action=user_mail_send',{email:_email,stmp_pwd:_passW,hosts:_server,port:_port},function(rdata){ - layer.close(loadT); - layer.msg(rdata.msg,{icon:rdata.status?1:2}) - }) - }) - $(".smtp_closeBtn").click(function(){ - layer.close(index) - }) - } - }) - }); -} -function select_port(port){ - switch(port){ - case '25': - return true; - case '465': - return true; - case '587': - return true; - case '': - return true; - default: - return false - } -} -function get_channel_settings(callback){ - var loadT = layer.msg('Getting profile, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - $.post('/config?action=get_settings',function(rdata){ - layer.close(loadT); - if (callback) callback(rdata); - }) -} -function add_receive_info(){ - layer.open({ - type: 1, - area: "400px", - title: "Add recipient email", - closeBtn: 2, - shift: 5, - shadeClose: false, - content: '
\ -
\ - Recipient mailbox\ -
\ - \ -
\ -
\ -
\ - \ - \ -
\ -
', - success:function(layers,index){ - $(".CreaterReceive").click(function(){ - var _receive = $('input[name=creater_email_value]').val(),_that = this; - if(_receive != ''){ - var loadT = layer.msg('Please wait while creating recipient list...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - layer.close(index) - $.post('/config?action=add_mail_address',{email:_receive},function(rdata){ - layer.close(loadT); - // 刷新收件列表 - get_receive_list(); - layer.msg(rdata.msg,{icon:rdata.status?1:2}); - }) - }else{ - layer.msg('Recipient mailbox cannot be empty!',{icon:2}); - } - }) - - $(".smtp_closeBtn").click(function(){ - layer.close(index) - }) - } - }) -} -function get_receive_list(){ - $.post('/config?action=get_settings',function(rdata){ - var _html = '',_list = rdata.user_mail.mail_list; - if(_list.length > 0){ - for(var i= 0; i<_list.length;i++){ - _html += '\ - '+ _list[i] +'\ - Del\ - ' - } - }else{ - _html = 'No Data' - } - $('#receive_table').html(_html); - }) - -} - -function del_email(mail){ - var loadT = layer.msg('Deleting ['+ mail +'], please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }),_this = this; - $.post('/config?action=del_mail_list',{email:mail},function(rdata){ - layer.close(loadT); - layer.msg(rdata.msg,{icon:rdata.status?1:2}) - _this.get_receive_list() - }) -} -// 设置钉钉 -function SetChannelDing(){ - var _url = $('textarea[name=channel_dingding_value]').val(); - var _all = $('#panel_alert_all').prop("checked"); - if(_url != ''){ - var loadT = layer.msg('Please wait while generating dingding channel...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - $.post('/config?action=set_dingding',{url:_url,atall:_all == true? 'True':'False'},function(rdata){ - layer.close(loadT); - layer.msg(rdata.msg,{icon:rdata.status?1:2}) - }) - }else{ - layer.msg('Please enter the dingding URL',{icon:2}) - } -} diff --git a/BTPanel/static/js/crontab.js b/BTPanel/static/js/crontab.js index 4324b967..b741f71d 100644 --- a/BTPanel/static/js/crontab.js +++ b/BTPanel/static/js/crontab.js @@ -147,29 +147,29 @@ function edit_task_info(id){ } }); } - if(obj.from.notice_channel == 'dingding') { - obj.sBody.title = '钉钉' + if(obj.from.notice_channel == 'telegram') { + obj.sBody.title = 'Telegram' }else if(obj.from.notice_channel == 'mail') { obj.sBody.title = 'Email' - }else if(obj.from.notice_channel == 'dingding,mail') { + }else if(obj.from.notice_channel == 'telegram,mail') { obj.sBody.title = 'All' } else { obj.sBody.title = 'No Data' } if(obj.from.sType == 'site' || obj.from.sType == 'database' || obj.from.sType == 'path') { - $.post('/config?action=get_settings',{type: 'sites'},function(rdata){ - if(rdata.user_mail.user_name && rdata.dingding.dingding) { + $.post('/config?action=get_settings2',{type: 'sites'},function(rdata){ + if(rdata.user_mail.user_name && rdata.telegram.setup) { obj.sBody.messageChannelBtnText = 'All' - obj.sBody.channelInitVal= 'user_name,dingding' - obj.sBody.messageChannelDom = '
  • All
  • 钉钉
  • 邮箱
  • ' - } else if(!rdata.user_mail.user_name && !rdata.dingding.dingding){ + obj.sBody.channelInitVal= 'user_name,telegram' + obj.sBody.messageChannelDom = '
  • All
  • Telegram
  • Email
  • ' + } else if(!rdata.user_mail.user_name && !rdata.telegram.setup){ obj.sBody.messageChannelBtnText = 'No Data' obj.sBody.channelInitVal= '' obj.sBody.messageChannelDom += '
  • No Data
  • ' - } else if(rdata.dingding.dingding) { - obj.sBody.messageChannelBtnText = '钉钉' - obj.sBody.channelInitVal= 'dingding' - obj.sBody.messageChannelDom += '
  • 钉钉
  • ' + } else if(rdata.telegram.setup) { + obj.sBody.messageChannelBtnText = 'Telegram' + obj.sBody.channelInitVal= 'telegram' + obj.sBody.messageChannelDom += '
  • Telegram
  • ' } else if(rdata.user_mail.user_name) { obj.sBody.messageChannelBtnText = 'Email' obj.sBody.channelInitVal= 'mail' @@ -465,7 +465,7 @@ function edit_task_info(id){ } // 修改木马查杀 消息通道 function edit_message_channel(type){ - $.post('/config?action=get_settings',function(res){ + $.post('/config?action=get_settings2',function(res){ var tMess = ""; if(res.user_mail.user_name && !res.dingding.dingding){ tMess = '
    \ @@ -975,20 +975,20 @@ function toBackup(type){ \
    '; if (type == 'sites' || type == 'path' || type == 'databases') { - $.post('/config?action=get_settings',data,function(rdata){ + $.post('/config?action=get_settings2',data,function(rdata){ var messageChannelDom = '', messageChannelBtnText = '', channelInitVal = '' - if(rdata.user_mail.user_name && rdata.dingding.dingding) { + if(rdata.user_mail.user_name && rdata.telegram.setup) { messageChannelBtnText = 'ALL' - channelInitVal= 'user_name,dingding' - messageChannelDom = '
  • ALL
  • 钉钉
  • 邮箱
  • ' - } else if(!rdata.user_mail.user_name && !rdata.dingding.dingding){ + channelInitVal= 'user_name,telegram' + messageChannelDom = '
  • ALL
  • Telegram
  • Email
  • ' + } else if(!rdata.user_mail.user_name && !rdata.telegram.setup){ messageChannelBtnText = 'No Data' channelInitVal= '' messageChannelDom += '
  • No Data
  • ' - } else if(rdata.dingding.dingding) { + } else if(rdata.telegram.setup) { messageChannelBtnText = '钉钉' - channelInitVal= 'dingding' - messageChannelDom += '
  • 钉钉
  • ' + channelInitVal= 'telegram' + messageChannelDom += '
  • Telegram
  • ' } else if(rdata.user_mail.user_name) { messageChannelBtnText = 'Email' channelInitVal= 'mail' @@ -1016,7 +1016,7 @@ function toBackup(type){ \ \ \ - Set notifications\ + Set notifications\ \

    '; if(type == 'sites' || type == "path") { @@ -1203,20 +1203,20 @@ function webShell(){ }) } function message_channel_start(){ - $.post('/config?action=get_settings',function(res){ + $.post('/config?action=get_settings2',function(res){ var wBody = ""; - if(!res.user_mail.user_name && !res.dingding.dingding){ + if(!res.user_mail.user_name && !res.telegram.setup){ wBody = 'No message channel is set, please go to panel settings to add a message channel configuration?'; $(".plan-submit").css({"pointer-events":"none","background-color":"#e6e6e6","color":"#333"}); - }else if(res.user_mail.user_name && !res.dingding.dingding){ + }else if(res.user_mail.user_name && !res.telegram.setup){ wBody = '
    \ \ \
    ' - }else if(!res.user_mail.user_name && res.dingding.dingding){ + }else if(!res.user_mail.user_name && res.telegram.setup){ wBody = '
    \ - \ - \ + \ + \
    ' }else{ wBody ='
    \ @@ -1224,8 +1224,8 @@ function message_channel_start(){ \
    \
    \ - \ - \ + \ + \
    ' } $(".message_start").html(wBody); @@ -1253,259 +1253,6 @@ function fileupload(){ $("#sFile").click(); } -// 计划任务2021/3/24新增任务通知新增 -function open_three_channel_auth(){ - get_channel_settings(function(rdata){ - var isOpen = rdata.dingding.info.msg.isAtAll == 'True' ? 'checked': ''; - var isDing = rdata.dingding.info.msg == 'No Data'? '': rdata.dingding.info.msg.dingding_url; - layer.open({ - type: 1, - area: "600px", - title: "Set notifications", - closeBtn: 2, - shift: 5, - shadeClose: false, - content: '
    \ -
    \ -
    \ -

    Email

    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ - \ - \ -
    \ -
    \ -
    \ -
    EmailOPT
    \ -
    \ -
    \ -
    \ -
    \ -
    \ - \ -
    \ -
    \ -
    \ -
    ' - }) - $(".bt-w-menu p").click(function () { - var index = $(this).index(); - $(this).addClass('bgw').siblings().removeClass('bgw'); - $('.conter_box').eq(index).show().siblings().hide(); - }); - get_receive_list(); - }) -} -function add_receive_info(){ - layer.open({ - type: 1, - area: "400px", - title: "Add recipient email", - closeBtn: 2, - shift: 5, - shadeClose: false, - content: '
    \ -
    \ - Recipient mailbox\n\ -
    \ - \ -
    \ -
    \ -
    \ - \ - \ -
    \ -
    ', - success:function(layers,index){ - $(".CreaterReceive").click(function(){ - var _receive = $('input[name=creater_email_value]').val(),_that = this; - if(_receive != ''){ - var loadT = layer.msg('Creating recipient list, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - layer.close(index) - $.post('/config?action=add_mail_address',{email:_receive},function(rdata){ - layer.close(loadT); - // 刷新收件列表 - get_receive_list(); - layer.msg(rdata.msg,{icon:rdata.status?1:2}); - }) - }else{ - layer.msg('The recipient mailbox cannot be empty!',{icon:2}); - } - }) - - $(".smtp_closeBtn").click(function(){ - layer.close(index) - }) - } - }) -} - -function sender_info_edit(){ - var loadT = layer.msg('Fetching configuration, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - $.post('/config?action=get_settings',function(rdata){ - layer.close(loadT); - var qq_mail = rdata.user_mail.info.msg.qq_mail == undefined ? '' : rdata.user_mail.info.msg.qq_mail, - qq_stmp_pwd = rdata.user_mail.info.msg.qq_stmp_pwd == undefined? '' : rdata.user_mail.info.msg.qq_stmp_pwd, - hosts = rdata.user_mail.info.msg.hosts == undefined? '' : rdata.user_mail.info.msg.hosts, - port = rdata.user_mail.info.msg.port == undefined? '' : rdata.user_mail.info.msg.port - layer.open({ - type: 1, - area: "480px", - title: "Set sender email information", - closeBtn: 2, - shift: 5, - shadeClose: false, - btn:['Save','Close'], - content: '
    \ -
    \ - Sender email\ -
    \ - \ -
    \ -
    \ -
    \ - smtp password\ -
    \ - \ -
    \ -
    \ -
    \ - smtp server\ -
    \ - \ -
    \ -
    \ -
    \ - smtp port\ -
    \ - \ - \ -
    \ -
    \ -
    ', - success:function(layers,index){ - var _option = ''; - if(select_port(port)){ - if(port == '465' || port == ''){ - _option = '' - }else if(port == '25'){ - _option = '' - }else{ - _option = '' - } - }else{ - _option = '' - } - console.log(port) - $("#port_select").html(_option) - $("#port_select").change(function(e){ - if(e.target.value == 'other'){ - // $("#port_select").css("width","100px"); - $('input[name=channel_email_port]').css("display","inline-block"); - }else{ - // $("#port_select").css("width","300px"); - $('input[name=channel_email_port]').css("display","none"); - } - }) - }, - yes:function(){ - var _email = $('input[name=channel_email_value]').val(); - var _passW = $('input[name=channel_email_password]').val(); - var _server = $('input[name=channel_email_server]').val(),_port - if($('#port_select').val() == 'other'){ - _port = $('input[name=channel_email_port]').val(); - }else{ - _port = $('#port_select').val() - } - if(_email == ''){ - return layer.msg('Email address cannot be empty!',{icon:2}); - }else if(_passW == ''){ - return layer.msg('STMP password cannot be empty!',{icon:2}); - }else if(_server == ''){ - return layer.msg('STMP server address cannot be empty!',{icon:2}) - }else if(_port == ''){ - return layer.msg('STMP server port cannot be empty!',{icon:2}) - } - var loadT = layer.msg('The notification is being generated, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - $.post('/config?action=user_mail_send',{email:_email,stmp_pwd:_passW,hosts:_server,port:_port},function(rdata){ - layer.close(loadT); - layer.msg(rdata.msg,{icon:rdata.status?1:2}) - if(rdata.status){ - layer.close(index) - get_channel_settings(); - } - }) - } - }) - }) -} - -function get_channel_settings(callback){ - var loadT = layer.msg('Fetching configuration, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); - $.post('/config?action=get_settings',function(rdata){ - layer.close(loadT); - if (callback) callback(rdata); - }) -} - -function get_receive_list(){ - $.post('/config?action=get_settings',function(rdata){ - var _html = '',_list = rdata.user_mail.mail_list; - if(_list.length > 0){ - for(var i= 0; i<_list.length;i++){ - _html += '\ - '+ _list[i] +'\ - Del\ - ' - } - }else{ - _html = 'No data' - } - $('#receive_table').html(_html); - }) - -} - -function del_email(mail){ - var loadT = layer.msg('Deleting ['+mail+'], please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }),_this = this; - $.post('/config?action=del_mail_list',{email:mail},function(rdata){ - layer.close(loadT); - layer.msg(rdata.msg,{icon:rdata.status?1:2}) - _this.get_receive_list() - }) -} - -// // 设置钉钉 -// function SetChannelDing(){ -// var _url = $('textarea[name=channel_dingding_value]').val(); -// var _all = $('#panel_alert_all').prop("checked"); -// if(_url != ''){ -// var loadT = layer.msg('正在生成钉钉通道中,请稍候...', { icon: 16, time: 0, shade: [0.3, '#000'] }); -// $.post('/config?action=set_dingding',{url:_url,atall:_all == true? 'True':'False'},function(rdata){ -// layer.close(loadT); -// layer.msg(rdata.msg,{icon:rdata.status?1:2}) -// }) -// }else{ -// layer.msg('请输入钉钉url',{icon:2}) -// } -// } function selSave_local() { if($('#save_local').val() == '0') { @@ -1547,20 +1294,3 @@ function getselectnoticename(){ } }); } - -function select_port(port){ - switch(port){ - case '25': - return true; - case '465': - return true; - case '587': - return true; - case '': - return true; - default: - return false - } -} - -// --计划任务2021/3/24新增任务通知新增结束 \ No newline at end of file diff --git a/BTPanel/static/js/database.js b/BTPanel/static/js/database.js index c8546eb0..43f0d1b6 100644 --- a/BTPanel/static/js/database.js +++ b/BTPanel/static/js/database.js @@ -1,71 +1,240 @@ - +var database_table = {} var database = { - get_list: function (page, search) { - if (page == undefined) page = 1; - if (!search) search = $("#SearchValue").val(); - bt.database.get_list(page, search, function (rdata) { - $('#databasePage').html(rdata.page); - var _tab = bt.render({ - table: '#DataBody', - columns: [ - { field: 'id', type: 'checkbox', width: 30 }, - { - field: 'name', title: lan.database.add_name, width: '20%' + init: function () { + this.database_table_view(); + var _this = this; + $('#SearchValue').keydown(function (e) { + if (e.keyCode == 13) { + var val = $(this).val(); + _this.database_table_view(val); + } + }); + }, + database_table_view:function(search){ + $('#bt_database_table').empty(); + database_table = bt_tools.table({ + el: '#bt_database_table', + url: '/data?action=getData', + param: { + table: 'databases', + search:search|| '' + }, //参数 + minWidth: '1000px', + default: "Database list is empty", // 数据为空时的默认提示 + column:[ + { fid: 'id', type: 'checkbox', width: 30 }, + { + fid: 'name', title: lan.database.add_name, width: '20%' + }, + { + fid: 'username', title: lan.database.user, sort: function () { + database_table.$refresh_table_list(true); + } + }, + { + fid:'password', + title:lan.database.add_pass, + type:'password', + copy:true, + eye_open:true + }, + { + fid:'backup', + title: lan.database.backup, + width: 130, + template: function (item) { + var backup = lan.database.backup_empty, + _class = "bt_warning"; + if (item.backup_count > 0) backup = lan.database.backup_ok, _class = "bt_success"; + return '' + backup + (item.backup_count > 0 ? ('(' + item.backup_count + ')') : '') + ' | ' + + ''+lan.database.input+''; + } + }, + // { + // fid: 'ps', title: lan.database.add_ps, templet: function (item) { + // var _ps = "" + // if (item.password) { + // _ps += item.ps + // } else { + // _ps += lan.database.cant_get_pass+''+lan.database.edit_pass+''+lan.database.button_set_pass+'!'; + // } + // _ps += ""; + // return _ps; + // } + // }, + { + fid: 'ps', + title: lan.database.add_ps, + type: 'input', + blur: function (row, index, ev) { + bt.pub.set_data_ps({ + id: row.id, + table: 'databases', + ps: ev.target.value + }, function (res) { + layer.msg(res.msg, (res.status ? {} : { + icon: 2 + })); + }); }, - { - field: 'username', title: lan.database.user, sort: function () { - database.get_list(); + keyup: function (row, index, ev) { + if (ev.keyCode === 13) { + $(this).blur(); + } + } + }, + { + type: 'group', + title: lan.database.operation, + width: 280, + align: 'right', + group: [{ + title: lan.database.admin, + tips: lan.database.admin_title, + event: function(row) { + bt.database.open_phpmyadmin(row.name,row.username,row.password); + } + },{ + title: lan.database.auth, + tips:lan.database.set_db_auth, + event: function(row) { + bt.database.set_data_access(row.username); + } + },{ + title:lan.database.tools, + tips:lan.database.mysql_tools, + event: function(row){ + database.rep_tools(row.name); + } + },{ + title:lan.database.edit_pass, + tips:lan.database.edit_pass_title, + event: function(row){ + database.set_data_pass(row.id,row.username,row.password); + } + },{ + title:lan.database.del, + tips:lan.database.del_title, + event: function(row){ + database.del_database(row.id,row.name); + } + }] + } + ], + sortParam: function (data) { + return { + 'order': data.name + ' ' + data.sort + }; + }, + tootls: [{ // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [{ + title: lan.database.add_title, + active: true, + event: function () { + bt.database.add_database(function (res){ + if(res.status) database_table.$refresh_table_list(true); + }) + } + },{ + title: lan.database.edit_root, + event: function () { + bt.database.set_root('root') + } + },{ + title: 'phpMyAdmin', + event: function () { + bt.database.open_phpmyadmin('','root', bt.config.mysql_root) + } + },{ + title: 'Sync all', + style: {'margin-left':'30px'}, + event: function () { + database.sync_to_database(1) + } + },{ + title: 'Get DB from server', + event: function () { + database.sync_to_database(1) + } + }] + },{ + type: 'batch', //batch_btn + positon: ['left', 'bottom'], + placeholder: 'Select batch operation', + buttonValue: 'Execute', + disabledSelectValue: 'Select the website to execute!!', + selectList: [{ + title:'Sync to Server', + url:'/database?action=SyncToDatabases&type=1', + paramName: 'ids', //列表参数名,可以为空 + paramId: 'id', // 需要传入批量的id + th:'Database Name', + beforeRequest: function(list) { + var arry = []; + $.each(list, function (index, item) { + arry.push(item.id); + }); + return JSON.stringify(arry) + }, + success: function (res, list, that) { + layer.closeAll(); + var html = ''; + $.each(list, function (index, item) { + html += '' + item.name + '
    ' + res.msg + '
    '; + }); + that.$batch_success_table({ + title: 'Batch sync selected', + th: 'Database Name', + html: html + }); + } + },{ + title: "Delete database", + url: '/database?action=DeleteDatabase', + load: true, + param: function (row) { + return { + id: row.id, + name: row.name } }, - { - field: 'password', title: lan.database.add_pass, templet: function (item) { - var _html = '**********'; - _html += ''; - _html += ''; - return _html; + callback: function (that) { // 手动执行,data参数包含所有选中的站点 + var ids = []; + for (var i = 0; i < that.check_list.length; i++) { + ids.push(that.check_list[i].id); } - }, - { - field: 'backup', title: lan.database.backup, templet: function (item) { - var backup = ''; - var _msg = lan.database.backup_empty; - if (item.backup_count > 0) _msg = lan.database.backup_ok; - backup += "" + _msg + " | " - backup += "" + lan.database.input + ""; - backup += ''; - return backup; - } - }, - { - field: 'ps', title: lan.database.add_ps, templet: function (item) { - var _ps = "" - if (item.password) { - _ps += item.ps - } else { - _ps += lan.database.cant_get_pass+''+lan.database.edit_pass+''+lan.database.button_set_pass+'!'; - } - _ps += ""; - return _ps; - } - }, - { - field: 'opt', title: lan.database.operation, align: 'right', templet: function (item) { - var option = ""+lan.database.admin+" | "; - option += ""+lan.database.tools+" | "; - option += ""+lan.database.auth+" | "; - option += ""+lan.database.edit_pass+" | "; - option += ""+lan.database.del+""; - return option; - } - }, - ], - data: rdata.data - }); - $(window).resize(function() { - database.forSize(); - }); - database.forSize(); - }) + database.del_database(ids,function(param){ + that.start_batch(param, function (list) { + layer.closeAll() + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += '' + item.name + '
    ' + item.request.msg + '
    '; + } + database_table.$batch_success_table({ + title: 'Batch deletion', + th: 'Database Name', + html: html + }); + }); + database_table.$refresh_table_list(true); + }) + } + }] + }, { //分页显示 + type: 'page', + positon: ['right', 'bottom'], // 默认在右下角 + pageParam: 'p', //分页请求字段,默认为 : p + page: 1, //当前分页 默认:1 + numberParam: 'limit', //分页数量请求字段默认为 : limit + number: 20, //分页数量默认 : 20条 + numberList: [10, 20, 50, 100, 200], // 分页显示数量列表 + numberStatus: true, // 是否支持分页数量选择,默认禁用 + jump: true, //是否支持跳转分页,默认禁用 + }] + }); }, rep_tools: function (db_name, res) { var loadT = layer.msg(lan.database.get_data, { icon: 16, time: 0 }); @@ -75,7 +244,7 @@ var database = { layer.msg(rdata.msg, { icon: 2 }); return; } - var types = { InnoDB: "MyISAM", MyISAM: "InnoDB" }; + var types = { InnoDB: "InnoDB", MyISAM: "MyISAM" }; var tbody = ''; for (var i = 0; i < rdata.tables.length; i++) { if (!types[rdata.tables[i].type]) continue; @@ -218,17 +387,17 @@ var database = { if (!isNaN($(this).val())) data.push($(this).val()); }); bt.database.sync_to_database({ type: type, ids: JSON.stringify(data) }, function (rdata) { - if (rdata.status) database.get_list(); + if (rdata.status) database_table.$refresh_table_list(true); }); }, sync_database: function () { bt.database.sync_database(function (rdata) { - if (rdata.status) database.get_list(); + if (rdata.status) database_table.$refresh_table_list(true); }) }, add_database: function () { bt.database.add_database(function (rdata) { - if (rdata.status) database.get_list(); + if (rdata.status) database_table.$refresh_table_list(true); }) }, batch_database: function (type, arr, result) { @@ -249,7 +418,7 @@ var database = { switch (type) { case 'del': if (arr.length < 1) { - database.get_list(); + database_table.$refresh_table_list(true); bt.msg({ msg: lan.get('del_all_database_ok', [result.count]), icon: 1, time: 5000 }); return; } @@ -265,17 +434,130 @@ var database = { break; } }, - del_database: function (id, name) { - bt.show_confirm(lan.get('del', [name]), lan.get('confirm_del', [name]), function () { - bt.database.del_database({ id: id, name: name }, function (rdata) { - if (rdata.status) database.get_list(); - bt.msg(rdata); - }) - }); + del_database: function (wid, dbname, callback) { + var rendom = bt.get_random_code(),num1 = rendom['num1'],num2 = rendom['num2'],title = ''; + title = typeof dbname === "function" ?'Batch delete databases':'Delete database [ '+ dbname +' ]'; + layer.open({ + type:1, + title:title, + icon:0, + skin:'delete_site_layer', + area: "530px", + closeBtn: 2, + shadeClose: true, + content:"
    " + + "" + + "
    The deletion may affect the business!
    " + + "
    Note: The data is priceless, please operate with caution! ! !"+(!recycle_bin_db_open?'
    Risk: The database recycle bin is not enabled, deleting the database will disappear forever!':'')+"
    " + + "
    " + lan.bt.cal_msg + ""+ num1 +" + "+ num2 +"=
    " + + "
    ", + btn:[lan.public.ok,lan.public.cancel], + yes:function(indexs){ + var vcodeResult = $('#vcodeResult'),data = {id: wid,name: dbname}; + if(vcodeResult.val() === ''){ + layer.tips('Calculation result cannot be empty', vcodeResult, {tips: [1, 'red'],time:3000}) + vcodeResult.focus() + return false; + }else if(parseInt(vcodeResult.val()) !== (num1 + num2)){ + layer.tips('Incorrect calculation result', vcodeResult, {tips: [1, 'red'],time:3000}) + vcodeResult.focus() + return false; + } + if(typeof dbname === "function"){ + delete data.id; + delete data.name; + } + layer.close(indexs) + var arrs = wid instanceof Array ? wid : [wid] + var ids = JSON.stringify(arrs), countDown = 9; + if (arrs.length == 1) countDown = 4 + title = typeof dbname === "function" ?'Confirm the information again, delete the database in batches':'Confirm the information again, Delete Database [ ' + dbname + ' ]'; + var loadT = bt.load('Checking database data information, please wait...') + bt.send('check_del_data', 'database/check_del_data', {ids: ids}, function (res) { + loadT.close() + layer.open({ + type:1, + title:title, + closeBtn: 2, + skin: 'verify_site_layer_info active', + area: '740px', + content: '
    ' + + '' + + '
    aaPanel kindly reminds you, please calm down for a few seconds, and then confirm whether you want to delete the data.
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    Risk: The database recycle bin is not enabled. After the database is deleted, the database will disappear forever!
    ' + + '
    Please read the above information to be deleted carefully to prevent the database from being deleted by mistake. Confirm the deletion and there is still ' + countDown + ' seconds to operate.
    ' + + '
    ', + btn: ['Delete (Can be operated after ' + countDown + ' seconds)', 'Cancel'], + success: function (layers) { + var html = '', rdata = res.data; + var filterData = rdata.filter(function(el){ + return ids.indexOf(el.id) != -1 + }) + for (var i = 0; i < filterData.length; i++) { + var item = filterData[i], newTime = parseInt(new Date().getTime() / 1000), + t_icon = ''; + + database_html = (function(item){ + var is_time_rule = (newTime - item.st_time) > (86400 * 30) && (item.total > 1024 * 10), + is_database_rule = res.db_size <= item.total, + database_time = bt.format_data(item.st_time, 'yyyy-MM-dd'), + database_size = bt.format_size(item.total); + + var f_size = ' ' + database_size + ' ' + (is_database_rule ? t_icon : ''); + var t_size = 'Note: This database is large and may be important data. Please operate with caution.\nDatabase: ' + database_size; + + return '
    ' + + 'Database: ' + item.name + '' + + ' 0 ? 'title="' + t_size+'"' : '')+'>Size: ' + f_size +'' + + 'Ctime:' + database_time + '' + + '
    ' + }(item)) + if(database_html !== '') html += '
    ' + database_html +'
    '; + } + if(html === '') html = '
    No data
    ' + $('.check_layer_content').html(html) + var interVal = setInterval(function () { + countDown--; + $(layers).find('.layui-layer-btn0').text('Delete (Can be operated after ' + countDown + ' seconds)') + $(layers).find('.check_layer_message span').text(countDown) + }, 1000); + setTimeout(function () { + $(layers).find('.layui-layer-btn0').text('Delete'); + $(layers).find('.check_layer_message').html('Note: Please read carefully the above information to be deleted to prevent the database from being deleted by mistake') + $(layers).removeClass('active'); + clearInterval(interVal) + }, countDown * 1000) + }, + yes:function(indes,layers){ + if($(layers).hasClass('active')){ + layer.tips('Please confirm the message, there are '+ countDown +' seconds left', $(layers).find('.layui-layer-btn0') , {tips: [1, 'red'],time:3000}) + return; + } + if(typeof dbname === "function"){ + dbname(data) + }else{ + bt.database.del_database(data, function (rdata) { + layer.closeAll() + if(rdata.status) database_table.$refresh_table_list(true); + if (callback) callback(rdata); + bt.msg(rdata); + }) + } + } + }) + }) + } + }) }, set_data_pass: function (id, username, password) { var bs = bt.database.set_data_pass(function (rdata) { - if (rdata.status) database.get_list(); + if(rdata.status) database_table.$refresh_table_list(true); bt.msg(rdata); }) $('.name' + bs).val(username); @@ -333,7 +615,7 @@ var database = { $('#btn_data_backup').unbind('click').click(function () { bt.database.backup_data(id, dataname, function (rdata) { if (rdata.status) database.database_detail(id, dataname); - database.get_list(); + if(rdata.status) database_table.$refresh_table_list(true); if (!rdata.status) layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); }) }) @@ -345,39 +627,6 @@ var database = { bt_upload_file.open(path, '.sql,.gz,.tar.gz,.zip', lan.database.input_up_type, function () { database.input_database(name); }); - - /* - var index = layer.open({ - type: 1, - closeBtn: 2, - title: lan.files.up_title + ' --- ' + lan.database.input_up_type + '', - area: ['500px', '500px'], - shadeClose: false, - content: '
    \ - \ - \ - \ - \ - \ - '+ lan.files.up_coding + ':\ - \ - \ - \ -
    ' - , end: function () { - database.input_database(name); - } - }); - $("#filesClose").click(function () { - layer.close(index); - database.input_database(name); - }); - UploadStart(true); - */ }, input_database: function (name) { var path = bt.get_cookie('backup_path') + "/database"; @@ -400,7 +649,7 @@ var database = { shift: 5, shadeClose: false, content: '
    ' - + '
    ' + + '
    ' + '
    ' + bt.render_help([lan.database.input_ps1, lan.database.input_ps2, (bt.os != 'Linux' ? lan.database.input_ps3.replace(/\/www.*\/database/, path) : lan.database.input_ps3)]) + '
    ' @@ -410,19 +659,19 @@ var database = { var _tab = bt.render({ table: '#DataInputList', columns: [ - { field: 'name', title: lan.files.file_name }, + { field: 'name', title: lan.files.file_name, width:190 }, { - field: 'etime', title: lan.files.file_etime, templet: function (item) { + field: 'etime', title: lan.files.file_etime, width:130, templet: function (item) { return bt.format_data(item.etime); } }, { - field: 'size', title: lan.files.file_size, templet: function (item) { + field: 'size', title: lan.files.file_size, width:70, templet: function (item) { return bt.format_size(item.size) } }, { - field: 'opt', title: 'Operating', align: 'right', templet: function (item) { + field: 'opt', title: 'Operating', align: 'right', width:90, templet: function (item) { return ''+lan.database.input+' | Del'; } }, @@ -448,4 +697,5 @@ var database = { td_width = ticket_with*0.8-30-$('#DataBody th:eq(2)').width()-$('#DataBody th:eq(3)').width()-$('#DataBody th:eq(4)').width()-$('#DataBody th:eq(6)').width(); $('#DataBody .webNote').css('max-width',td_width); } -} \ No newline at end of file +} +database.init(); \ No newline at end of file diff --git a/BTPanel/static/js/files.js b/BTPanel/static/js/files.js index 4333559f..19789171 100644 --- a/BTPanel/static/js/files.js +++ b/BTPanel/static/js/files.js @@ -220,7 +220,7 @@ var bt_file = { content:'
    '+ (html!=''?(''):'Please drag the file here'+ (!that.is_webkit?'The current browser does not support drag upload. Commend to use Chrome browser or WebKit kernel for browsing':'') +'') +'
    ', success:function(layers){ $('#mask_layer').hide(); - layers.find('.layui-layer-btn2').css('margin', '0 300px 0 0'); + layers.find('.layui-layer-btn2').css('float', 'left'); $('.file_dir_uploads .layui-layer-max').hide(); $('.upload_btn_groud .upload_file_btn').click(function(){$('.upload_btn_groud .dropdown-menu [data-type=file]').click()}); $('.upload_btn_groud .dropdown-menu a').click(function(){ @@ -725,6 +725,7 @@ var bt_file = { if(!res.msg) layer.msg('Refresh succeeded') }); }); + // 上传 $('.upload_file').on('click',function(e){ that.file_drop.dialog_view(); }); @@ -1117,7 +1118,7 @@ var bt_file = { left: ev.clientX - $(this).offset().left }; // 鼠标按下后拖动 - $(document).unbind('mousemove').mousemove(function(ev){ + bt_file.window_mousemove = function(ev) { // 鼠标按下后移动到的位置 var endPos = { top: ev.clientY - con_t > 0 && ev.clientY - con_t < container.height() ? ev.clientY - con_t : (ev.clientY - (con_t+container.height()) > 1 ?container.height():0), @@ -1130,6 +1131,7 @@ var bt_file = { if(bt.get_cookie('rank') == 'list'){ //在列表模式下减去表头高度 fixedPoint.top = fixedPoint.top + 40 } + var enter_files_box = that.enter_files_box() // 拖拽范围的宽高 var w = Math.min(Math.abs(endPos.left - startPos.left), con_l + container.width() - fixedPoint.left); var h = Math.min(Math.abs(endPos.top - startPos.top), con_t + container.height() - fixedPoint.top); @@ -1155,17 +1157,17 @@ var bt_file = { } if(startPos.top == endPos.top || startPos.left == endPos.left) return true; // 设置拖拽盒子位置 - that.enter_files_box().show().css({ + enter_files_box.show().css({ left: fixedPoint.left+'px', top: fixedPoint.top+'px', width: w+'px', height: h+'px' }); - var box_offset_top = that.enter_files_box().offset().top; - var box_offset_left = that.enter_files_box().offset().left; - var box_offset_w = that.enter_files_box().offset().left + that.enter_files_box().width(); - var box_offset_h = that.enter_files_box().offset().top + that.enter_files_box().height(); + var box_offset_top = enter_files_box.offset().top; + var box_offset_left = enter_files_box.offset().left; + var box_offset_w = enter_files_box.offset().left + enter_files_box.width(); + var box_offset_h = enter_files_box.offset().top + enter_files_box.height(); $(container).find('.file_tr').each(function(i,item){ var offset_top = $(item).offset().top; var offset_left = $(item).offset().left; @@ -1185,15 +1187,15 @@ var bt_file = { } } }); - }) + } // 鼠标抬起 - $(document).on('mouseup',function(){ - var _move_array = []; - var box_offset_top = that.enter_files_box().offset().top; - var box_offset_left = that.enter_files_box().offset().left; - var box_offset_w = that.enter_files_box().offset().left + that.enter_files_box().width(); - var box_offset_h = that.enter_files_box().offset().top + that.enter_files_box().height(); + bt_file.window_mouseup = function() { + var _move_array = [],enter_files_box = that.enter_files_box(); + var box_offset_top = enter_files_box.offset().top; + var box_offset_left = enter_files_box.offset().left; + var box_offset_w = enter_files_box.offset().left + enter_files_box.width(); + var box_offset_h = enter_files_box.offset().top + enter_files_box.height(); $(container).find('.file_tr').each(function(i,item){ var offset_top = $(item).offset().top; var offset_left = $(item).offset().left; @@ -1211,9 +1213,12 @@ var bt_file = { } }); that.render_file_selected(_move_array); //渲染数据 - that.enter_files_box().remove(); //删除盒子 + enter_files_box.remove(); //删除盒子 $('.file_list_content').unbind('mousewheel'); //解绑滚轮事件 - }) + $(document).unbind('mousemove',bt_file.window_mousemove); + } + $(document).one('mouseup',bt_file.window_mouseup); + $(document).on('mousemove',bt_file.window_mousemove); ev.stopPropagation(); ev.preventDefault(); }) @@ -1264,6 +1269,10 @@ var bt_file = { item.type_tips = item.type == 'file'?'File':'Directory'; that.file_groud_event(item); }); + // 文件搜索 + $('.replace_content').on('click', function() { + that.replace_content_view() + }) }, /** * @descripttion: 文件拖拽范围 @@ -1578,7 +1587,7 @@ var bt_file = { shadeClose: false, skin:'download_file_view', content:html[0].outerHTML, - btn:['Comfirm','Close'], + btn:['Confirm','Close'], success:function(){ form.setEvent(); }, @@ -1822,7 +1831,7 @@ var bt_file = { * @return: 无返回值 */ render_file_selected:function(_array){ - $(document).unbind('mouseup').unbind('mousemove'); + // $(document).unbind('mouseup').unbind('mousemove'); var that = this,tmp = []; that.clear_table_active() $.each(_array,function(index,item){ @@ -1930,7 +1939,7 @@ var bt_file = { */ del_favorites:function(path){ var that = this - layer.confirm('Comfirm delete path【'+path+'】?', { title: 'Delete favorites', closeBtn: 2, icon: 3 }, function (index) { + layer.confirm('Confirm delete path【'+path+'】?', { title: 'Delete favorites', closeBtn: 2, icon: 3 }, function (index) { that.$http('del_files_store',{path:path},function(res){ if(res.status){ that.render_favorites_type_list(); @@ -2519,6 +2528,219 @@ var bt_file = { }) }, + /** + * @description 文件内容搜索替换 + * @return void + */ + replace_content_view: function() { + layer.open({ + title: 'Search Files Content', + type: 1, + skin: 'replace_content_view', + area: '710px', + zIndex:19900, + closeBtn: 2, + content: '
    ' + + '
    ' + + 'Search' + + '
    ' + + '' + + ''+ + '' + + '
      '+ + '
      ' + + '
      ' + + '
      ' + + 'Suffix' + + '
      ' + + '' + + '
      ' + + '
      ' + + '
      ' + + 'Folder' + + '
      ' + + '' + + '
      ' + + ''+ + 'Subdir' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + 'Mode' + + '
      ' + + '
      ' + + '
      ' + + ''+ + 'Words' + + '
      ' + + '
      ' + + ''+ + 'Regex' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + ''+ + 'Match whole word' + + '
      ' + + '
      ' + + ''+ + 'Match case' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      '+ + '
      '+ + '
      '+ + '
      '+ + '
      Type search content to search in the file
      Options to narrow down the search
      '+ + '
      '+ + '' + + '
      ', + success:function(){ + //单选、复选框按钮事件 + $('.checkbox_config,.file_path_switch').click(function(e){ + if(e.target.localName == 'i' || e.target.localName == 'span'){ + var is_radio = $(this).find('i').hasClass('file_find_radio'),i_box = $(this).find('i'); + if(is_radio){//是否单选 + i_box.addClass('active').parent('div').siblings().find('i').removeClass('active'); + if($(this).find('.laberText').text() == 'Words'){ + $('.hide_option').removeClass('hide') + }else{ //正则模式下取消全词匹配 + $('.hide_option').addClass('hide').find('i').removeClass('active') + } + }else{//是否复选 + if(i_box.hasClass('active')){ + i_box.removeClass('active') + }else{ + i_box.addClass('active') + } + } + } + }) + // 历史输入 + $('.history_search').click(function(e){ + var list = JSON.parse(bt.get_cookie('file_search_list')),h_html = ''; + if($.type(list) === 'undefined' || list == null){ + h_html = 'No records'; + $('.history_search_list').html(h_html).removeClass('hide'); + }else{ + $('.history_search_list').empty(); + $.each(list,function(index,item){ + $('.history_search_list').append($('
    • ').attr('data-key',item).text(item)).removeClass('hide') + }) + } + $(document).one('click', function() { + $('.history_search_list').addClass('hide'); + e.stopPropagation(); + }); + e.stopPropagation(); + }) + //选择历史输入 + $('.history_search_list').on('click','li',function(){ + $('#replaceContentValue').val($(this).data('key')); + $('.history_search_list').addClass('hide'); + }) + } + }) + }, + /** + * @description 文件内容搜索結果 + * @returns void + */ + searchReplaceContent: function() { + var that = this, + file_num = 0, //文件数量 + match_num = 0, //文件内查询到的数量 + match_file_html = '', + data = { + text: $('#replaceContentValue').val(), + exts: $("#replaceFileExtsType").val() || 'html,php', + path: $('#replaceContentPath').val(), //路径 + is_subdir: !$('.replaceHasChild').find('i').hasClass('active') ? '0' : '1', //0不包含子目录 1 包含子目录 + mode: !$('.regularMatchRe').find('i').hasClass('active') ? '0' : '1', //为普通模式 1 为正则模式 + isword: !$('.allMatchRe').find('i').hasClass('active') ? '0' : '1', //全词匹配 0 默认 + iscase: !$('.distinguishCaseRe').find('i').hasClass('active') ? '1' : '0', //不区分大小写 0 默认 + noword: '0' //不输出行信息 0 默认 + } + this.$http('files_search',data,function(res) { + var reg = new RegExp("("+that.escodeChange(data.text)+")"); + if(res.error) return layer.msg(res.error, {icon: 2}); + if (data.text.indexOf('\n') < 0 && data.text != '') that.setSearchHistoryList(data.text); //设置搜索历史列表 + if($.isEmptyObject(res)){ + $('.replace_content_view .matchContent_main').html('
      No data found
      ') + $('.matchRresult').html('') + return + } + $.each(res,function(fileName,item){ + file_num++; + var contentNum = Object.keys(item).length + match_file_html+='
      '+ + '
      '+ + ''+fileName+'  (Match '+contentNum+' times)'+ + 'Edit'+ + '
      '+ + '
      ' + $.each(item,function(index,lineItem){ + match_num++ + var html = $('
      ').text(lineItem.trim()).html().replace(reg,''+ $('
      ').text(data.text).html() +'
      ') + match_file_html += '
      line '+index+':    '+html+'
      '; + }) + match_file_html+= '
      ' + }) + $('.matchRresult').html('Search results: '+ match_num +' matches in '+ file_num +' files') + $('.matchContent_main').html(match_file_html); + // 隐藏显示内容 + $('.matchContent_main .match_result_file_title').click(function(e){ + var parent_box = $(this).parents('.match_content_item'), + is_icon_top = $(this).find('i').hasClass('glyphicon-triangle-top') //是否图标向上(未打开) + if(is_icon_top){ + parent_box.find('.match_result_file_content').addClass('matchShow') + $(this).find('i').removeClass('glyphicon-triangle-top').addClass('glyphicon-triangle-bottom') + }else{ + $(this).find('i').removeClass('glyphicon-triangle-bottom').addClass('glyphicon-triangle-top') + parent_box.find('.match_result_file_content').removeClass('matchShow') + } + e.stopPropagation() + }) + //编辑跳转 + $('.editFile').click(function(){ + openEditorView(0,$(this).data('filename'),function(val,aceEitor){ + aceEitor.ace.find(data.text) + aceEitor.ace.execCommand('find') + }) + }) + }) + }, + /** + * @description 设置搜索历史列表 + * @param {String} text 查找的文本 + * @returns void + */ + setSearchHistoryList:function(text){ + var h_cookie = JSON.parse(bt.get_cookie('file_search_list')); + if($.type(h_cookie) === 'undefined' || h_cookie == null){ + bt.set_cookie('file_search_list', JSON.stringify([text])) + }else{ + if($.inArray(text,h_cookie) != -1) return true; //如果已在列表中则跳过 + h_cookie.unshift(text) //数组首位添加查找内容 + if(h_cookie.length > 7) h_cookie.pop() //超过7位时删除最后一条搜索记录 + bt.set_cookie('file_search_list', JSON.stringify(h_cookie)) + } + }, + /** + * @description 转义查找输入的内容特殊字符 + * @param {String} e 查找的内容 + * @returns 返回转义结果 + */ + escodeChange:function(e){ + if(/(\+|\-|\$|\||\!|\(|\)|\{|\}|\[|\]|\^|\”|\~|\*|\?|\:|\\)/g.test(e)){ + e = e.replace(/(\+|\-|\$|\||\!|\(|\)|\{|\}|\[|\]|\^|\”|\~|\*|\?|\:|\\)/g,'\\$1').replace(/&/g, "&").replace(/\>/g, ">").replace(/\Comfirm delete '+ data.type_tips +'[ '+ data.path +' ],it will move to recycle bin after delete, continue?' + msg:'Confirm delete '+ data.type_tips +'[ '+ data.path +' ],it will move to recycle bin after delete, continue?' },function(){ that.del_file_req(data,function(res){ that.reader_file_list({path:that.file_path}) @@ -4519,7 +4741,7 @@ var bt_file = { */ remove_download_url:function(data,callback){ var that = this; - layer.confirm('Comfirm to stop sharing【'+ data.fileName +'】, continue?',{ title: 'Cancel sharing', closeBtn: 2, icon: 3 }, function () { + layer.confirm('Confirm to stop sharing【'+ data.fileName +'】, continue?',{ title: 'Cancel sharing', closeBtn: 2, icon: 3 }, function () { this.layerT = bt.load('Canceling sharing files, please wait...'); bt.send('remove_download_url','files/remove_download_url',{id:data.id},function(res){ if (callback) callback(res); @@ -4787,7 +5009,7 @@ var bt_file = { shadeClose: false, closeBtn:2, skin:'unpack_file_view', - btn:['Comfirm','Cancel'], + btn:['Confirm','Cancel'], content: html[0].outerHTML, success:function(){ if(data.ext == 'gz') _type = 'tar' //解压格式 diff --git a/BTPanel/static/js/public.js b/BTPanel/static/js/public.js index 9dfbe9f6..d3b0529e 100644 --- a/BTPanel/static/js/public.js +++ b/BTPanel/static/js/public.js @@ -3815,7 +3815,7 @@ function messagebox(){ function get_message_data(page,callback){ if(typeof page === "function") callback = page,page = 1; - var loadT = bt.load('正在获取消息列表,请稍后...'); + var loadT = bt.load('Getting message list, please wait...'); bt.send("getData","data/getData",{ tojs:'reader_message_list', table:'tasks', @@ -4893,4 +4893,299 @@ acme = { if (callback) callback(res) }); } -} \ No newline at end of file +} + + + +/** 消息通道 **/ +function MessageChannelSettings(){ + MessageChannel.get_channel_settings(function (rdata){ + layer.open({ + type: 1, + area: "600px", + title: "Setting up notification", + skin:"layer-channel-auth", + closeBtn: 2, + shift: 5, + shadeClose: false, + content: '
      \ +
      \ +
      \ +

      Email

      \ +

      Telegram

      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ + \ + \ +
      \ +
      \ +
      \ +
      EmailOperating
      \ +
      \ +
      \ +
      \ +
      \ +
      \ + \ +
      \ +
      \ +
      \ +
      ', + success:function(){ + $('.addTelegram').click(function(){ + var _id = $('[name=telegram_id]').val(),_token = $('[name=telegram_token]').val(); + if(_id == '' || _token == '') return layer.msg('input box cannot be empty!'); + var loadT = layer.msg('The notification is being generated, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=set_tg_bot',{bot_token:_token,my_id:_id},function(rdata){ + layer.close(loadT); + layer.msg(rdata.msg,{icon:rdata.status?1:2}) + }) + }) + $('.delTelegram').click(function(){ + var loadTs = layer.msg('Deleting notification, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=del_tg_info',function(rdata){ + layer.close(loadTs); + layer.msg(rdata.msg,{icon:rdata.status?1:2}) + if(rdata.status){ + $('[name=telegram_id]').val(''); + $('[name=telegram_token]').val('') + $('.delTelegram').hide(); + } + }) + }) + } + }) + $(".bt-w-menu p").click(function () { + var index = $(this).index(); + $(this).addClass('bgw').siblings().removeClass('bgw'); + $('.conter_box').eq(index).show().siblings().hide(); + }); + MessageChannel.get_receive_list(); + }) +} +var MessageChannel = { + //获取推送设置 + get_channel_settings:function(callback){ + var loadT = layer.msg('Getting profile, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=get_settings2',function(rdata){ + layer.close(loadT); + if (callback) callback(rdata); + }) + }, + // 获取收件者列表 + get_receive_list:function(){ + $.post('/config?action=get_settings2',function(rdata){ + var _html = '',_list = rdata.user_mail.mail_list; + if(_list.length > 0){ + for(var i= 0; i<_list.length;i++){ + _html += '\ + '+ _list[i] +'\ + Del\ + ' + } + }else{ + _html = 'No Data' + } + $('#receive_table').html(_html); + }) + }, + // 添加收件者 + add_receive_info:function (){ + var _this = this + layer.open({ + type: 1, + area: "400px", + title: "Add recipient email", + closeBtn: 2, + shift: 5, + shadeClose: false, + content: '
      \ +
      \ + Recipient mailbox\ +
      \ + \ +
      \ +
      \ +
      \ + \ + \ +
      \ +
      ', + success:function(layers,index){ + $(".CreaterReceive").click(function(){ + var _receive = $('input[name=creater_email_value]').val(); + if(_receive != ''){ + var loadT = layer.msg('Please wait while creating recipient list...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + layer.close(index) + $.post('/config?action=add_mail_address',{email:_receive},function(rdata){ + layer.close(loadT); + // 刷新收件列表 + _this.get_receive_list(); + layer.msg(rdata.msg,{icon:rdata.status?1:2}); + }) + }else{ + layer.msg('Recipient mailbox cannot be empty!!',{icon:2}); + } + }) + + $(".smtp_closeBtn").click(function(){ + layer.close(index) + }) + } + }) + }, + // 删除收件者 + del_email:function(mail){ + var loadT = layer.msg('Deleting['+mail+'],please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }),_this = this; + $.post('/config?action=del_mail_list',{email:mail},function(rdata){ + layer.close(loadT); + layer.msg(rdata.msg,{icon:rdata.status?1:2}) + _this.get_receive_list() + }) + }, + // 设置发送者邮箱信息 + sender_info_edit:function (){ + var loadT = layer.msg('Getting profile, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=get_settings2',function(rdata){ + layer.close(loadT); + var qq_mail = rdata.user_mail.info.msg.qq_mail ? rdata.user_mail.info.msg.qq_mail:'', + qq_stmp_pwd = rdata.user_mail.info.msg.qq_stmp_pwd? rdata.user_mail.info.msg.qq_stmp_pwd:'', + hosts = rdata.user_mail.info.msg.hosts? rdata.user_mail.info.msg.hosts:'', + port = rdata.user_mail.info.msg.port? rdata.user_mail.info.msg.port:'', + is_custom = $.inArray(port,['25','465','587','']) != -1 //是否自定义 + layer.open({ + type: 1, + area: "460px", + title: "Set sender email information", + closeBtn: 2, + shift: 5, + shadeClose: false, + content: '
      \ +
      \ + Sender email\ +
      \ + \ +
      \ +
      \ +
      \ + SMTP password\ +
      \ + \ +
      \ +
      \ +
      \ + SMTP server\ +
      \ + \ +
      \ +
      \ +
      \ + SMTP port\ +
      \ + \ + \ +
      \ +
      \ + \ +
      \ + '+(qq_mail != ''?'':'')+'\ + \ +
      \ +
      ', + success:function(layers,index){ + var _option = ''; + if(is_custom){ + if(port == '465' || port == ''){ + _option = '' + }else if(port == '25'){ + _option = '' + }else{ + _option = '' + } + }else{ + _option = '' + } + $("#port_select").html(_option) + $("#port_select").change(function(e){ + if(e.target.value == 'other'){ + $("#port_select").css("width","100px"); + $('input[name=channel_email_port]').css("display","inline-block"); + }else{ + $("#port_select").css("width","300px"); + $('input[name=channel_email_port]').css("display","none"); + } + }) + $(".SetChannelEmail").click(function(){ + var _email = $('input[name=channel_email_value]').val(); + var _passW = $('input[name=channel_email_password]').val(); + var _server = $('input[name=channel_email_server]').val(), + _port = '' + if($('#port_select').val() == 'other'){ + _port = $('input[name=channel_email_port]').val(); + }else{ + _port = $('#port_select').val() + } + if(!_email) return layer.msg('Email address cannot be empty!',{icon:2}); + if(!_passW) return layer.msg('STMP password cannot be empty!',{icon:2}); + if(!_server)return layer.msg('STMP server address cannot be empty!',{icon:2}) + if(!_port) return layer.msg('STMP server port cannot be empty!',{icon:2}) + + var loadT = layer.msg('The notification is being generated, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=user_mail_send',{email:_email,stmp_pwd:_passW,hosts:_server,port:_port},function(rdata){ + layer.close(loadT); + if(rdata.status){ + layer.close(index) + MessageChannel.get_channel_settings(); + } + layer.msg(rdata.msg,{icon:rdata.status?1:2}) + }) + }) + $(".smtp_closeBtn").click(function(){ + layer.close(index) + }) + $('.set_empty').click(function(){ + var loadTs = layer.msg('notification, please wait...', { icon: 16, time: 0, shade: [0.3, '#000'] }); + $.post('/config?action=set_empty',{type:'mail'},function(rdata){ + layer.close(loadTs); + layer.msg(rdata.msg,{icon:rdata.status?1:2}) + if(rdata.status){ + layer.close(index) + } + }) + }) + } + }) + }) + } +} +/** 消息通道 end**/ \ No newline at end of file diff --git a/BTPanel/static/js/public_backup.js b/BTPanel/static/js/public_backup.js index 252374f8..a2252198 100644 --- a/BTPanel/static/js/public_backup.js +++ b/BTPanel/static/js/public_backup.js @@ -227,6 +227,25 @@ var bt = { var num = min + Math.round(rand * range); //四舍五入 return num; }, + + /** + * 生成计算数字(加强计算,用于删除重要数据二次确认) + * */ + get_random_code: function () { + var flist = [20, 21, 22, 23] + + var num1 = bt.get_random_num(13, 19); + var t1 = num1 % 10; + + var num2 = bt.get_random_num(13, 29); + var t2 = num2 % 10; + + while ($.inArray(num2, flist) >= 0 || (t1 + t2) <= 10 || t1 == t2) { + num2 = bt.get_random_num(13, 29); + t2 = num2 % 10; + } + return { 'num1': num1, 'num2': num2 } + }, /** * @description 设置本地存储,local和session * @param {String} type 存储类型,可以为空,默认为session类型。 @@ -294,9 +313,16 @@ var bt = { return null; } }, - select_path: function(id, type) { + /** + * @description 选择文件目录或文件 + * @param id {string} 元素ID + * @param type {string || function} 选择方式,文件或目录 + * @param success {function} 成功后的回调 + */ + select_path: function(id, type,success,default_path) { _this = this; _this.set_cookie("SetName", ""); + if(typeof type !== 'string') success = type,type = 'dir'; var loadT = bt.open({ type: 1, area: "680px", @@ -322,40 +348,64 @@ var bt = { $("." + id).val(path).change(); loadT.close(); }) - } - }); - _this.set_cookie('ChangePath', loadT.form); - var paths = $("#" + id).val(); - if ($('#defaultPath').length > 0 && $("#" + id).parents('.tab-body').length > 0) { - paths = $('#defaultPath').text(); - } - _this.get_file_list(paths, type); + //选择 + $('#bt_select').on('click',function () { + var path = bt.format_path($("#PathPlace").find("span").text()); + if(type === 'file' && !$('#tbody tr.active').length){ + layer.msg('Select the file first!',{icon:0}) + return false; + } + if ($('#tbody tr').hasClass('active')) { + path = $('#tbody tr.active .bt_open_dir').attr('path'); + } + path = bt.rtrim(path, '/'); + $("#" + id).val(path).change(); + $("." + id).val(path).change(); + if(typeof success === "function") success(path) + loadT.close(); + }) + var element = $("#" + id),paths = element.val(),defaultPath = $('#defaultPath'); + if (defaultPath.length > 0 && element.parents('.tab-body').length > 0) { + paths = defaultPath.text(); + } + if(default_path){ + paths = default_path; + } + _this.get_file_list(paths, type); + } + }); + _this.set_cookie('ChangePath', loadT.form); + // var paths = $("#" + id).val(); + // if ($('#defaultPath').length > 0 && $("#" + id).parents('.tab-body').length > 0) { + // paths = $('#defaultPath').text(); + // } + // _this.get_file_list(paths, type); - function ActiveDisk() { - var a = $("#PathPlace").find("span").text().substring(0, 1); - switch (a) { - case "C": - $(".path-con-left dd:nth-of-type(1)").css("background", "#eee").siblings().removeAttr("style"); - break; - case "D": - $(".path-con-left dd:nth-of-type(2)").css("background", "#eee").siblings().removeAttr("style"); - break; - case "E": - $(".path-con-left dd:nth-of-type(3)").css("background", "#eee").siblings().removeAttr("style"); - break; - case "F": - $(".path-con-left dd:nth-of-type(4)").css("background", "#eee").siblings().removeAttr("style"); - break; - case "G": - $(".path-con-left dd:nth-of-type(5)").css("background", "#eee").siblings().removeAttr("style"); - break; - case "H": - $(".path-con-left dd:nth-of-type(6)").css("background", "#eee").siblings().removeAttr("style"); - break; - default: - $(".path-con-left dd").removeAttr("style") - } - } + // function ActiveDisk() { + // var a = $("#PathPlace").find("span").text().substring(0, 1); + // switch (a) { + // case "C": + // $(".path-con-left dd:nth-of-type(1)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "D": + // $(".path-con-left dd:nth-of-type(2)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "E": + // $(".path-con-left dd:nth-of-type(3)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "F": + // $(".path-con-left dd:nth-of-type(4)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "G": + // $(".path-con-left dd:nth-of-type(5)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // case "H": + // $(".path-con-left dd:nth-of-type(6)").css("background", "#eee").siblings().removeAttr("style"); + // break; + // default: + // $(".path-con-left dd").removeAttr("style") + // } + // } }, get_file_list:function(path, type){ var _that = this; @@ -424,9 +474,52 @@ var bt = { }) }) }, + prompt_confirm: function (title, msg, callback) { + layer.open({ + type: 1, + title: title, + area: "480px", + closeBtn: 2, + btn: ['OK', 'Cancel'], + content: "
      \ +

      " + msg + "

      \ +
      \ + \ +
      If you confirm the operation, enter it manually '" + title + "'
      \ +
      \ +
      ", + success: function () { + var black_txt_ = $('#prompt_input_box') - - + $('.placeholder').click(function () { + $(this).hide().siblings('input').focus() + }) + black_txt_.focus(function () { + $('.prompt_input_tips.placeholder').hide() + }) + black_txt_.blur(function () { + black_txt_.val() == '' ? $('.prompt_input_tips.placeholder').show() : $('.prompt_input_tips.placeholder').hide() + }); + black_txt_.keyup(function () { + if (black_txt_.val() == '') { + $('.prompt_input_tips.placeholder').show(); + $('.prompt_input_ps').hide(); + } else { + $('.prompt_input_tips.placeholder').hide(); + } + }) + }, + yes: function (layers, index) { + var result = $("#prompt_input_box").val().trim(); + if (result == title) { + layer.close(layers) + if (callback) callback() + } else { + $('.prompt_input_ps').show(); + } + } + }); + }, show_confirm: function(title, msg, fun, error) { if (error == undefined) { error = "" @@ -683,7 +776,7 @@ var bt = { _tab.click(function() { $('#' + obj).find('span').removeClass('on'); $(this).addClass('on'); - var _contents = $('.tab-con'); + var _contents = $('#' + obj).next('.tab-con'); _contents.html(''); $(this).data('callback')(_contents); }) @@ -3072,14 +3165,25 @@ bt.system = { }, rep_panel: function(callback) { var loading = bt.load(lan.index.rep_panel_the) - bt.send('RepPanel', 'system/RepPanel', {}, function(rdata) { - loading.close(); - if (rdata) { - if (callback) callback({ status: rdata, msg: lan.index.rep_panel_ok }); - bt.system.reload_panel(); + $.ajax({ + type: 'POST', + url: 'system?action=RepPanel', + error: function (err) { + setTimeout(() => { + loading.close(); + bt.system.reload_panel(function () { + location.reload(); + }); + }, 1000 * 60 * 5); + }, + success: function (rdata) { + loading.close(); + if (rdata) { + if (callback) callback({ status: rdata, msg: lan.index.rep_panel_ok }); + bt.system.reload_panel(); + } } - - }) + }); }, get_warning: function(callback) { bt.send('GetWarning', 'ajax/GetWarning', {}, function(rdata) { @@ -3884,10 +3988,7 @@ bt.soft = { bt.soft.pro.get_product_discount_by(config.pid,function(rdata){ //rdata = {"36": {"discount": 1, "did": 0, "price": 3564, "name": "正常", "sprice": 3564}, "24": {"discount": 1, "did": 0, "price": 2376, "name": "正常", "sprice": 2376}, "12": {"discount": 1, "did": 0, "price": 1188, "name": "正常", "sprice": 1188}, "6": {"discount": 1, "did": 0, "price": 594, "name": "正常", "sprice": 594}, "3": {"discount": 1, "did": 0, "price": 297, "name": "正常", "sprice": 297}, "1": {"discount": 1, "did": 0, "price": 99, "name": "正常", "sprice": 99}, "pid": "100000045"}; if(typeof rdata.status === "boolean"){ - if(!rdata.status) { - bt.msg({status:false, msg:rdata.msg}) - return false; - } + if(!rdata.status) return false; } that.product_cache[config.pid] = rdata; setTimeout(function(){ delete that.product_cache[config.pid] },60000); @@ -4021,8 +4122,7 @@ bt.soft = { if (rdata.status === false){ bt.set_cookie('force', 1); if (soft) soft.flush_cache(); - // layer.msg(rdata.msg, { icon: 2 }); - bt.msg({status:false,msg:rdata.msg}) + layer.msg(rdata.msg, { icon: 2 }); return; } config.pay = parseInt($('#libPay-mode .pay-cycle-btn.active').data('condition')); @@ -4486,8 +4586,7 @@ bt.soft = { if (rdata.status === false) { bt.set_cookie('force', 1); if (soft) soft.flush_cache(); - // layer.msg(rdata.msg, { icon: 2 }); - bt.msg({status:false,msg:rdata.msg}) + layer.msg(rdata.msg, { icon: 2 }); return; } $(".pay-wx").html(''); diff --git a/BTPanel/static/js/site.js b/BTPanel/static/js/site.js index d3c2745c..d42198cf 100644 --- a/BTPanel/static/js/site.js +++ b/BTPanel/static/js/site.js @@ -1,325 +1,1924 @@ -var site_table = bt_tools.table({ - el:'#bt_site_table', - url:'/data?action=getData', - cookiePrefix:'site_table', // cookie前缀,用于状态存储,如果不设置,着所有状态不存储, - param:{table:'sites'}, //参数 - minWidth:'1000px', - autoHeight:true, - default:"Site list is empty", // 数据为空时的默认提示 - beforeRequest:function(param){ - param.type = bt.get_cookie('site_type') || -1; - return param; - }, - column:[ - {type:'checkbox',class:'',width:20}, - {fid:'name',title:lan.site.site_name,sort:true,sortValue:'asc',type:'link',event:function(row,index,ev){ - site.web_edit(row,true); - }}, - {fid:'status',title:lan.site.status,sort:true,width:85,config:{icon:true,list:[['1',lan.site.running_text,'bt_success','glyphicon-play'],['0',lan.site.stopped,'bt_danger','glyphicon-pause']]},type:'status',event:function(row,index,ev,key,that){ - bt.site[parseInt(row.status)?'stop':'start'](row.id,row.name,function(res){ - if(res.status) that.$modify_row_data({status:parseInt(row.status)?'0':'1'}); - }); - }}, - {fid:'backup_count',title:lan.site.backup,width:80,type:'link',template:function(row,index){ - var backup = lan.site.backup_no,_class = "bt_warning"; - if (row.backup_count > 0) backup = lan.site.backup_yes,_class = "bt_success"; - return ''+ backup + (row.backup_count >0?('('+ row.backup_count +')'):'') +''; - },event:function(row,index){ - site.site_detail(row.id,row.name); - }}, - {fid:'path',title:lan.site.root_dir,tips:'Open path',type:'link',event:function(row,index,ev){ - openPath(row.path); - }}, - {fid:'edate',title:lan.site.endtime,width:115,class:'set_site_edate',sort:true,type:'link',template:function(row,index){ - var _endtime = ''; - if (row.edate) _endtime = row.edate; - if (row.endtime) _endtime = row.endtime; - _endtime = (_endtime === "0000-00-00") ? lan.site.web_end_time : _endtime; - return _endtime; - },event:function(row){}}, //模拟点击误删 - {fid:'ps',title:lan.site.note,type:'input',blur:function(row,index,ev){ - bt.pub.set_data_ps({id:row.id,table:'sites',ps:ev.target.value},function(res){ - if(!res.status) layer.msg(res.msg,{status:2}); - }); - },keyup:function(row,index,ev){ - if(ev.keyCode === 13){ - $(this).blur(); - } - }}, - {fid:'php_version',title:'PHP',tips:'Selete php version',width:57,type:'link',template:function(row,index){ - if(row.php_version.indexOf('static') > -1) return row.php_version; - return row.php_version; - },event:function(row,index){ - site.web_edit(row); - setTimeout(function(){ - $('.site-menu p:eq(9)').click(); - },500); - }}, - {fid:'ssl',title:'SSL',tips:'Deployment certificate',width:110,type:'text',template:function(row,index){ - var _ssl = row.ssl,_info = '',_arry = [['issuer','Certificate'],['notAfter','Due date'],['notBefore','Application date'],['dns','Domain name']]; - for(var i=0;i<_arry.length;i++){ - var item = _ssl[_arry[i][0]]; - _info += _arry[i][1]+':'+ item + (_arry.length-1 != i?'\n':''); - } - return _ssl === -1?'Not Set': (_ssl.endtime >= 0? 'Expire: '+ _ssl.endtime +'days':'Expired'); - },event:function(row,index,ev,key,that){ - site.web_edit(row); - setTimeout(function(){ - $('.site-menu p:eq(8)').click(); - },500); - }}, - {title:lan.site.operate,type:'group',width:119,align:'right',group:[ - { - title:'WAF', - event:function(row,index,ev,key,that){ - site.site_waf(row.name); - } - }, - { - title:lan.site.set, - event:function(row,index,ev,key,that){ - site.web_edit(row,true); - } - },{ - title:'Del', - event:function(row,index,ev,key,that){ - site.del_site(row.id,row.name,function(){ - that.$refresh_table_list(true); - }); - } - }]} - ], - sortParam:function(data){ - return {'order':data.name +' '+ data.sort}; - }, - // 表格渲染完成后 - success:function(that){ - $('.event-edate-'+ that.random).each(function(){ - var $this = $(this); - laydate.render({ - elem: $this[0] //指定元素 - , min: bt.get_date(1) - , max: '2099-12-31' - , vlue: bt.get_date(365) - , type: 'date' - , format: 'yyyy-MM-dd' - , trigger: 'click' - , btns: ['perpetual', 'confirm'] - , theme: '#20a53a' - , ready:function(){ - $this.click(); - } - , done: function (date) { - var item = that.event_rows_model.rows; - bt.site.set_endtime(item.id, date,function(res){ - if(res.status){ - layer.msg(res.msg); - return false; - } - bt.msg(res); - }); - } - }); - }); - }, - // 渲染完成 - tootls:[{ // 按钮组 - type:'group', - positon:['left','top'], - list:[ - {title:'Add site',active:true, event:function(ev){ site.add_site(function(){ - site_table.$refresh_table_list(true) }); - bt.set_cookie('site_type','-1'); - }}, - {title:'Default Page',event:function(ev){ site.set_default_page() }}, - {title:'Default Website',event:function(ev){ site.set_default_site() }}, - {title:'PHP CLI version',event:function(ev){ site.get_cli_version()}}, - {title:'Category manager',group:true,init:function(className){ - bt.site.get_type(function(res){ - var html = ''; - $.each(res,function(index,item){ - html += '
    • '+ item.name +'
    • '; - }); - html += '
    • Category set
    • '; - $('.' + className).next().html(html); - $('.' + className).next('ul').on('click','li a',function(){ - var id = $(this).data('id'); - if(id == 'type_sets'){ - site.set_class_type(); - }else{ - bt.set_cookie('site_type',id); - site_table.$refresh_table_list(true); - } - }); - }); - }} - ] - },{ // 搜索内容 - type:'search', - positon:['right','top'], - placeholder:'Please enter domain or remarks', - searchParam:'search', //搜索请求字段,默认为 search - value:'',// 当前内容,默认为空 - },{ // 批量操作 - type:'batch',//batch_btn - positon:['left','bottom'], - placeholder:'Select batch operation', - buttonValue:'Execute', - disabledSelectValue:'Select the website to execute!', - selectList:[ - { - group:[{title:lan.site.enable_website,param:{status:1}},{title:'Disable website',param:{status:0}}], - url:'/site?action=set_site_status_multiple', - confirmVerify:false, //是否提示验证方式 - paramName:'sites_id', //列表参数名,可以为空 - paramId:'id', // 需要传入批量的id - theadName:'Name' - },{ - title:lan.site.backup_website, - url:'/site?action=ToBackup', - paramId:'id', - load:true, - theadName:'Name', - callback:function(that){ // 手动执行,data参数包含所有选中的站点 - that.start_batch({},function(list){ - var html = ''; - for(var i=0;i
      '+ item.request.msg +'
      '; - } - site_table.$batch_success_table({title:'Batch backup',th:'Site name',html:html}); - site_table.$refresh_table_list(true); - }); - } - },{ - title:lan.site.set_expired, - url:'/site?action=set_site_etime_multiple', - paramName:'sites_id', //列表参数名,可以为空 - paramId:'id', // 需要传入批量的id - theadName:'Name', - confirm:{ - title:'Batch set expired date', - content:'
      Expired date
      ', - success:function(){ - laydate.render({ - elem: '#site_edate' - ,min: bt.format_data(new Date().getTime(),'yyyy-MM-dd') - ,max: '2099-12-31' - ,vlue: bt.get_date(365) - ,type: 'date' - ,format: 'yyyy-MM-dd' - ,trigger: 'click' - ,btns: ['perpetual','confirm'] - ,theme: '#20a53a' - }); - }, - yes:function(index,layers,request){ - var site_edate = $('#site_edate'),site_edate_val = site_edate.val(); - if(site_edate_val != ''){ - request({'edate':site_edate_val==='Forever'?'0000-00-00':site_edate_val}); - }else{ - layer.tips('Input expired date','#site_edate',{tips:['1','red']}); - $('#site_edate').css('border-color','red'); - $('#site_edate').click(); - setTimeout(function(){ - $('#site_edate').removeAttr('style'); - },3000); - return false; - } - } - } - },{ - title:lan.site.set_php_version, - url:'/site?action=set_site_php_version_multiple', - paramName:'sites_id', //列表参数名,可以为空 - paramId:'id', // 需要传入批量的id - theadName:'Name', - confirm:{ - title:'Batch set php version', - area:'420px', - content:'
      PHP version
      ', - success:function(){ - bt.site.get_all_phpversion(function(res){ - var html = ''; - $.each(res,function(index,item){ - html += ''; - }); - $('[name="versions"]').html(html); - }); - }, - yes:function(index,layers,request){ - request({version:$('[name="versions"]').val()}); - } - } - },{ - title:lan.site.set_category, - url:'/site?action=set_site_type', - paramName:'site_ids', //列表参数名,可以为空 - paramId:'id', // 需要传入批量的id - beforeRequest:function(list){ - var arry = []; - $.each(list,function(index,item){ - arry.push(item.id); - }); - return JSON.stringify(arry); - }, - confirm:{ - title:'Batch set category', - content:'
      Site category
      ', - success:function(){ - bt.site.get_type(function(res){ - var html = ''; - $.each(res,function(index,item){ - html += ''; - }); - $('[name="site_types"]').html(html); - }); - }, - yes:function(index,layers,request){ - request({id:$('[name="site_types"]').val()}); - } - }, - tips:false, - success:function(res,list,that){ - var html = ''; - $.each(list,function(index,item){ - html += ''+ item.name +'
      '+ res.msg +'
      '; - }); - that.$batch_success_table({title:'Batch set category',th:'Site name',html:html}); - that.$refresh_table_list(true); - } - },{ - title:lan.site.del_website, - url:'/site?action=delete_website_multiple', - paramName:'sites_id', //列表参数名,可以为空 - paramId:'id', //需要传入批量的id - theadName:'Name', - confirm:function(config,callback){ - bt.show_confirm("Delete site","Confirm delete the FTP、database、root path of the selected site with the same name", function(){ - var param = {}; - $('.bacth_options input[type=checkbox]').each(function(){ - var checked = $(this).is(":checked"); - if(checked) param[$(this).attr('name')] = checked?1:0; - }) - if(callback) callback(param); - },"
      "); - } - } - ], - },{ //分页显示 - type:'page', - positon:['right','bottom'], // 默认在右下角 - pageParam:'p', //分页请求字段,默认为 : p - page:1, //当前分页 默认:1 - numberParam:'limit', //分页数量请求字段默认为 : limit - number:20, //分页数量默认 : 20条 - numberList:[10,20,50,100,200], // 分页显示数量列表 - numberStatus:true, // 是否支持分页数量选择,默认禁用 - jump:true, //是否支持跳转分页,默认禁用 - }] -}); + +$('#cutMode span').on('click',function(){ + var index = $(this).index(); + $(this).addClass('on').siblings().removeClass('on'); + $(this).parent().next().find('.tab-con-block').eq(index).removeClass('hide').siblings().addClass('hide'); + switch(index){ + case 0: + $('#bt_site_table').empty(); + if(!isSetup) $('.site_table_view .mask_layer').removeClass('hide').find('.prompt_description').html('Web server is not installed,Install Nginx  |  Install Apache'); + site.php_table_view(); + site.get_types(); + break; + case 1: + $('#bt_node_table').empty(); + $.get('/plugin?action=getConfigHtml',{name: "nodejs"},function(res){ + if(typeof res !== 'string') $('.site_table_view .mask_layer').removeClass('hide').find('.prompt_description').html('Node version manager is not installed,Click install'); + }) + site.node_porject_view(); + break; + } + bt.set_cookie('site_tab_status',index==0?'php':'nodejs') +}) + var site = { + node:{ + /** + * @description 选择路径配置 + * @return config {object} 选中文件配置 + * + */ + get_project_select_path:function(path){ + var that = this; + return { + type: 'text', + width: '320px', + name: 'project_script', + value:path, + placeholder:'Please select the project startup file and enter the startup command. It cannot be empty', + icon: { + type: 'glyphicon-folder-open', + select:'file', + event: function (ev){} + } + } + }, + get_project_select:function(path){ + var that = this; + return { + type: 'select', + name: 'project_script', + width: '200px', + disabled: true, + unit: '* Get the startup mode in package.json', + placeholder: 'Select the project continue', + list:path?function(configs){ + that.get_project_script_list(path,configs[2],this) + }:[], + change:function(formData,elements,formConfig){ + var project_script = $("[data-name=\'project_script\']"); + if(formData.project_script === ''){ + if($("#project_script_two").length === 0){ + project_script.parent().after('
      ') + } + }else{ + project_script.parent().next().remove(); + } + } + } + }, + + /** + * @description 选择启动脚本配置 + * @param path {string} 项目目录 + * @param form {object} 表单元素 + * @param formObject {object} 表单对象 + * @return config {object} 选中文件配置 + */ + get_project_script_list:function(path,form,formObject){ + var that = this; + that.get_start_command({project_cwd: path},function(res){ + var arry = []; + for (var resKey in res) { + arry.push({title: resKey + ' 【' + res[resKey] + '】', value:resKey }) + } + arry.push({title:'Custom command',value:''}) + form.group = that.get_project_select(path); + form.group.list = arry; + form.group.disabled = false; + formObject.$replace_render_content(2) + if(arry.length === 1){ + var project_script = $("[data-name=\'project_script\']"); + // form.group.value = ''; + project_script.parent().after('
      ') + } + },function(){ + form.label = 'Start file/command'; + form.group = that.get_project_select_path(path); + formObject.$replace_render_content(2) + }) + return []; + }, + + /** + * + * @description 获取Node版本列表 + * @return {{dataFilter: (function(*): *[]), url: string}} + */ + get_node_version_list:function(){ + return { + url: '/project/nodejs/get_nodejs_version', + dataFilter: function(res) { + if(res.length === 0){ + layer.closeAll(); + bt.msg({status:false,msg:'Please open [Node Version Manager], install at least 1 Node version to continue'}) + return; + } + var arry = []; + for (var i = 0; i < res.length; i++) { + arry.push({title:res[i],value:res[i]}) + } + return arry; + } + } + }, + + + /** + * @description 获取Node通用Form配置 + * @param config {object} 获取配置参数 + * @return form模板 + */ + get_node_general_config: function (config) { + config = config || {} + var that = this, + formLineConfig = [{ + label: 'Path', + group: { + type: 'text', + width: '350px', + name: 'project_cwd', + readonly: true, + icon: { + type: 'glyphicon-folder-open', + event: function (ev) {}, + callback: function (path) { + var filename = path.split('/'); + var project_script_config = this.config.form[2], + project_name_config = this.config.form[1], + project_ps_config = this.config.form[6]; + project_name_config.group.value = filename[filename.length - 1]; + project_ps_config.group.value = filename[filename.length - 1]; + project_script_config.group.disabled = false; + this.$replace_render_content(1) + this.$replace_render_content(6) + that.get_project_script_list(path,project_script_config,this) + } + }, + value: bt.get_cookie('sites_path') ? bt.get_cookie('sites_path') : '/www/wwwroot', + placeholder: 'Please select the project directory' + } + }, { + label: 'Name', + group: { + type: 'text', + name: 'project_name', + width: '350px', + placeholder: 'Please enter the name of the Node project', + input: function (formData,formElement,formConfig) { + var project_ps_config = formConfig.config.form[6]; + project_ps_config.group.value = formData.project_name; + formConfig.$replace_render_content(6) + } + } + }, { + label: 'Run opt', + group:(function(){ + return that.get_project_select(config.path) + }()) + }, { + label: 'Port', + group: { + type: 'number', + name: 'port', + width: '200px', + placeholder: 'Port of the project', + unit: '* Port of the project', + } + }, { + label: 'User', + group: { + type: 'select', + name: 'run_user', + width: '150px', + unit:'* No special requirements,choose www user', + list: [{title:'www',value:'www'},{title: 'root', value: 'root'}], + tips: 'sssss' + } + }, { + label: 'Node', + group: { + type: 'select', + name: 'nodejs_version', + width: '150px', + unit: '* Choose the right Node version, Install other', + list: (function () { + return that.get_node_version_list() + })() + } + }, { + label: 'Remarks', + group: { + type: 'text', + name: 'project_ps', + width: '420px', + placeholder: 'Please enter project remarks', + value:config.ps, + } + }, { + label: 'Domain name', + group: { + type: 'textarea', //当前表单的类型 支持所有常规表单元素、和复合型的组合表单元素 + name: 'domains', //当前表单的name + style: {'width': '420px', 'height': '120px', 'line-height': '22px'}, + tips: { //使用hover的方式显示提示 + text: 'Please enter the domain name to be bound, this option can be empty
      One domain name per line, the default is port 80
      Pan-analysis adding method *.domain.com
      If the format of the additional port is www.domain.com:88', + style: {top: '10px', left: '15px'}, + } + } + },{ + group:{ + type:'help', + list:[ + '[Run opt]: The scripts list in package.json is read by default, or you can select the [Custom Command] option to manually enter the start command', + '[Custom start]: You can select the startup file or directly enter the startup command. Supported startup methods: npm/node/pm2/yarn', + '[Port]:The wrong port will lead to access to 502, if you dont know the port, you can change to the correct port after starting', + '[User]:For security reasons, the www user is used by default to run, and root user running may bring security risks' + ] + } + }] + + if (config.path) { + formLineConfig.splice(-1, 1) + return formLineConfig.concat([{ + label: 'Boot', + group: { + type: 'checkbox', + name: 'is_power_on', + width: '220px', + title: 'Follow the system to start the service', + } + }, { + label: '', + group: { + type: 'button', + name:'saveNodeConfig', + title: 'Save', + event: function (data,form,that) { + if(data.project_cwd === ''){ + bt.msg({status:false,msg:'The project directory cannot be empty'}) + return false + } + var project_script_two = $('[name="project_script_two"]'); + if(data.project_script === '' && project_script_two.length < 1 || project_script_two.length > 1 && project_script_two.val() === ''){ + bt.msg({status:false,msg:'Start file/command cannot be empty'}) + return false + } + if(data.port === ''){ + bt.msg({status:false,msg:'Project port cannot be empty'}) + return false + } + if(data.project_script === ''){ + data.project_script = project_script_two.val() + delete data.project_script_two + } + config.callback(data,form,that) + } + } + }]) + } + return formLineConfig; + }, + + /** + * @description 添加node项目表单 + * @returns {{form: 当前实例对象, close: function(): void}} + */ + add_node_form:function(callback){ + var that = this; + var add_node_project = bt_tools.open({ + title:"Add Node project", + area:'700px', + btn:['Confirm','Cancel'], + content:{ + class: 'pd30', + form:(function(){ + return that.get_node_general_config({ + form:add_node_project + }) + })() + }, + yes:function(form,indexs,layers){ + var defaultParam = { + bind_extranet:0, + is_power_on:1, + max_memory_limit:4096, + project_env:'' + } + if(form.domains !== ''){ + var arry = form.domains.replace('\n','').split('\r'),newArry = [] + for (var i = 0; i < arry.length; i++) { + var item = arry[i]; + if(bt.check_domain(item)){ + newArry.push(item.indexOf(':') > -1?item:item+':80') + }else{ + bt.msg({status:false,msg:'['+ item +'] The format of the bound domain name is incorrect'}) + break; + } + } + defaultParam.bind_extranet = 1 + defaultParam.domains = newArry + } + if(form.project_name === ''){ + bt.msg({status:false,msg:'Project name cannot be empty'}) + return false + } + var project_script_two = $('[name="project_script_two"]'); + if(project_script_two.length && project_script_two.val() === ''){ + bt.msg({status:false,msg:'Please enter a custom startup command, it cannot be empty!'}); + return false + } + if(form.port === ''){ + bt.msg({status:false,msg:'Project port cannot be empty'}) + return false + } + if(form.project_script === null){ + bt.msg({status:false,msg:'Please select the project directory to get the start command!'}) + return false + } + form = $.extend(form,defaultParam) + if(project_script_two.length){ + form.project_script = project_script_two.val() + delete form.project_script_two + } + var _command = null; + setTimeout(function(){ + if(_command < 0) return false; + _command = that.request_module_log_command({shell:'tail -f /www/server/panel/logs/npm-exec.log'}) + },500); + site.node.add_node_project(form,function(res){ + if(!res.status) _command = -1 + if(_command > 0) layer.close(_command) + if(callback) callback(res,indexs) + }) + } + }) + return add_node_project; + }, + + /** + * @description 添加node项目请求 + * @param param {object} 请求参数 + * @param callback {function} 回调函数 + */ + add_node_project:function(param,callback){ + this.http({create_project:false,verify:false},param,callback) + }, + + /** + * @description 获取Node环境 + * @param callback {function} 回调函数 + */ + get_node_environment:function(callback){ + bt_tools.send({ + url:'/project/nodejs/is_install_nodejs' + },function(res){ + if(callback) callback(res); + },{load:'Get the Node project environment'}) + }, + + /** + * @description 编辑Node项目请求 + * @param param {object} 请求参数 + * @param callback {function} 回调函数 + */ + modify_node_project:function(param,callback){ + this.http({modify_project: 'Modify Node project configuration'},param,callback) + }, + + /** + * @description 删除Node项目请求 + * @param param {object} 请求参数 + * @param callback {function} 回调函数 + */ + remove_node_project:function(param,callback){ + this.http({remove_project: 'Delete Node project'},param,callback) + }, + + /** + * @description 获取node项目域名 + * @param callback {function} 回调行数 + */ + get_node_project_domain:function(callback){ + this.http({project_get_domain: 'Get the list of Node project domain names'},callback) + }, + + /** + * @description 获取启动命令列表 + * @param param {object} 请求参数 + * @param callback {function} 成功回调行数 + * @param callback1 {function} 错误回调行数 + */ + get_start_command:function(params,callback,callback1){ + this.http({get_run_list: 'Getting project start command'},params,callback,callback1) + }, + /** + * @description 添加Node项目域名 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + add_node_project_domain:function(param,callback){ + this.http({project_add_domain:false,verify:false},param,callback) + }, + + /** + * @description 删除Node项目域名 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + remove_node_project_domain:function(param,callback){ + this.http({project_remove_domain:'Delete the Node project domain name'},param,callback) + }, + + /** + * @description 启动Node项目 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + start_node_project:function(param,callback){ + this.http({start_project:'Enable Node project'},param,callback) + }, + + /** + * @description 停止Node项目 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + stop_node_project:function(param,callback){ + this.http({stop_project:'Stop the Node project'},param,callback) + }, + + /** + * @description 重启Node项目 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + restart_node_project:function(param,callback){ + this.http({restart_project:'Restart the Node project'},param,callback) + }, + + /** + * @description 获取值指定Node项目信息 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + get_node_project_info:function(param,callback){ + this.http({get_project_info:'Get Node project information'},param,callback) + }, + + /** + * @description 绑定外网映射 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + bind_node_project_map:function(param,callback){ + this.http({bind_extranet:'Mapping',verify:false},param,callback) + }, + /** + * @description 绑定外网映射 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + unbind_node_project_map:function(param,callback){ + this.http({unbind_extranet:'Unmapping',verify:false},param,callback) + }, + /** + * @description 安装node项目依赖 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + install_node_project_packages:function(param,callback){ + this.http({install_packages:false,verify:false},param,callback) + }, + + /** + * @description 安装指定模块 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + npm_install_node_module:function(param,callback){ + this.http({install_module:'Install Node module'},param,callback) + }, + /** + * @description 更新指定模块 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + upgrade_node_module:function(param,callback){ + this.http({upgrade_module:'Update Node module'},param,callback) + },/** + * @description 删除指定模块 + * @param param {object} 请求参数 + * @param callback {function} 回调行数 + */ + uninstall_node_module:function(param,callback){ + this.http({uninstall_module:'Uninstall the Node module'},param,callback) + }, + /** + * @description 模拟点击 + */ + simulated_click:function(num){ + $('.bt-w-menu p:eq('+ num +')').click(); + }, + /** + * @description 获取Node项目信息 + * @param row {object} 当前行,项目信息 + */ + set_node_project_view:function(row){ + var that = this; + bt.open({ + type:1, + title:'Node project management-['+ row.name +'], add time ['+ row.addtime +']', + skin:'node_project_dialog', + area:['800px','720px'], + content:'
      ' + + ''+ + '
      ' + + '
      '+ + '
      Please turn on mapping to viewing the configuration information
      '+ + '
      ', + btn:false, + success:function(layers){ + var $layers = $(layers), $content = $layers.find('#webedit-con'); + function reander_tab_list(config) { + for (var i = 0; i < config.list.length; i++) { + var item = config.list[i],tab = $('

      ' + item.title + '

      '); + $(config.el).append(tab); + (function (i,item) { + tab.on('click',function(ev){ + $('.mask_module').addClass('hide'); + $(this).addClass('bgw').siblings().removeClass('bgw'); + if($(this).hasClass('bgw')) { + that.get_node_project_info({project_name:row.name},function(res){ + config.list[i].event.call(that,$content,res,ev) + }) + } + }) + if(item.active) tab.click() + }(i,item)) + } + } + reander_tab_list({ + el:$layers.find('.bt-w-menu'), + list:[{ + title:'Project config', + active:true, + event:that.reander_node_project_config + },{ + title:'Domain', + event:that.reander_node_domain_manage + },{ + title:'Mapping', + event:that.reander_node_project_map, + },{ + title:'URL rewrite', + event:that.reander_node_project_rewrite + },{ + title:'Config file', + event:that.reander_node_file_config + },{ + title:'SSL', + event:that.reander_node_project_ssl + },{ + title:'Load status', + event:that.reander_node_service_condition + },{ + title:'service status', + event:that.reander_node_service_status + },{ + title:'Module', + event:that.reander_node_project_module + },{ + title:'Project log', + event:that.reander_node_project_log + },{ + title:'Website log', + event:that.reander_node_site_log + }] + + }) + } + }) + }, + + /** + * @description 渲染Node项目配置视图 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + * @param that {object} 当前node项目对象 + */ + reander_node_project_config:function(el,rows){ + var row = $.extend(true,{},rows); + var that = this,edit_node_project = bt_tools.form({ + el:'#webedit-con', + data: row.project_config, + class: 'ptb10', + form:(function(){ + var fromConfig = that.get_node_general_config({ + form:edit_node_project, + path:row.path, + ps:row.ps, + callback:function(data,form,formNew){ + data['is_power_on'] = data['is_power_on']?1:0; + var project_script_two = $('[name="project_script_two"]'); + if(project_script_two.length && project_script_two.val() === ''){ + bt.msg({status:false,msg:'Please enter a custom startup command, it cannot be empty!'}); + return false + } + if(form.port === ''){ + bt.msg({status:false,msg:'Project port cannot be empty'}) + return false + } + if(form.project_script === null){ + bt.msg({status:false,msg:'Please select the project directory to get the start command!'}) + return false + } + site.node.modify_node_project(data,function(res){ + if(res.status){ + row['project_config'] = $.extend(row,data); + row['path'] = data.project_script; + row['ps'] = data.ps; + } + bt.msg({status:res.status, msg:res.data}) + site.node.simulated_click(0) + }) + } + }) + setTimeout(function(){ + var is_existence = false,list = fromConfig[2].group.list; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + if(item.value === rows.project_config.project_script){ + is_existence = true; + break; + } + } + if(!is_existence && list.length > 1){ + $('[data-name="project_script"] li:eq('+ (list.length-1) + ')').click() + $('[name="project_script_two"]').val(rows.project_config.project_script) + } + if(list.length === 1){ + $('[data-name="project_script"] li:eq(0)').click() + $('[name="project_script_two"]').val(rows.project_config.project_script) + } + },250) + + fromConfig[1].group.disabled = true; + fromConfig[fromConfig.length-3].hide = true; + fromConfig[fromConfig.length-3].group.disabled = true; + return fromConfig + })() + }) + setTimeout(function(){ + $(el).append('') + if(!row.listen_ok) $(el).find('input[name="port"]').parent().after('
      The project port may be wrong, it is detected that the current project listens to the following ports[ '+ row.listen.join('/') +' ]
      '); + + },100) + }, + + /** + * @description 渲染Node项目服务状态 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_service_status:function(el,row){ + var arry = [ + {title: 'Start', event: this.start_node_project}, + {title: 'Stop', event: this.stop_node_project}, + {title: 'Restart', event: this.restart_node_project} + ], + that = this, + html = $('

      '); + function reander_service(status){ + var status_info = status?['Start','#20a53a','play']:['Stop','red','pause']; + return 'Status: ' + status_info[0] + '' + } + html.find('.status').html(reander_service(row.run)) + el.html(html) + for (var i = 0; i < arry.length; i++) { + var item = arry[i], btn = $(''); + (function (btn, item,indexs) { + !(row.run && indexs === 0) || btn.addClass('hide'); + !(!row.run && indexs === 1 ) || btn.addClass('hide'); + btn.on('click', function () { + bt.confirm({ + title:item.title +'Project-['+ row.name +']', + msg:'Are you sure you want the'+ item.title +'item,'+ (row.run?'The project may be affected,':'') +'continue?' + },function(index){ + layer.close(index) + item.event.call(that, {project_name:row.name},function(res){ + row.run = (indexs === 0?true:(indexs === 1?false:row.run)) + html.find('.status').html(reander_service(row.run)) + $('.sfm-opt button').eq(0).addClass('hide'); + $('.sfm-opt button').eq(1).addClass('hide'); + $('.sfm-opt button').eq(row.run?1:0).removeClass('hide'); + bt.msg({status:res.status,msg:res.data || res.error_msg}) + }) + }) + }).text(item.title) + })(btn, item,i) + el.find('.sfm-opt').append(btn) + } + }, + + + /** + * @description 渲染Node项目域名管理 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_domain_manage:function(el,row){ + var that = this, + list = [{ + class: 'mb0', + items: [ + { name: 'nodedomain', width: '340px', type: 'textarea', placeholder: 'Please enter the domain name to be mapped, this option can be empty
      One domain name per line, default port 80
      How to add wildcard domain names *.domain.com
      Specify the port used www.domain.com:88' }, + { + name: 'btn_node_submit_domain', + text: 'Add', + type: 'button', + callback: function(sdata) { + var arrs = sdata.nodedomain.split("\n"); + var domins = []; + for (var i = 0; i < arrs.length; i++) domins.push(arrs[i]); + that.add_node_project_domain({project_name:row.name,domains:domins}, function(res) { + bt.msg({status:res.status,msg:res.data || res.error_msg}); + if(res.status){ + $('[name=nodedomain]').val(''); + $('.placeholder').css('display','block') + project_domian.$refresh_table_list(true); + } + }) + } + } + ] + }] + var _form_data = bt.render_form_line(list[0]),loadT = null,placeholder = null; + el.html(_form_data.html+'
      '); + bt.render_clicks(_form_data.clicks); + // domain样式 + $('.btn_node_submit_domain').addClass('pull-right').css("margin", "30px 35px 0 0"); + $('textarea[name=nodedomain]').css('height','120px') + placeholder = $(".placeholder"); + placeholder.click(function() { + $(this).hide(); + $('.nodedomain').focus(); + }).css({ 'width': '340px', 'heigth': '120px', 'left': '0px', 'top': '0px', 'padding-top': '10px', 'padding-left': '15px' }) + $('.nodedomain').focus(function() { + placeholder.hide(); + loadT = layer.tips(placeholder.html(), $(this), { tips: [1, '#20a53a'], time: 0, area: $(this).width() }); + }).blur(function() { + if ($(this).val().length == 0) placeholder.show(); + layer.close(loadT); + }); + var project_domian = bt_tools.table({ + el:'#project_domian_list', + url:'/project/nodejs/project_get_domain', + default: "No domain name list yet", + param:{project_name:row.name}, + height:375, + beforeRequest:function(params){ + if(params.hasOwnProperty('data') && typeof params.data === 'string') return params + return {data:JSON.stringify(params)} + }, + column:[{ type: 'checkbox', class: '', width: 20 }, { + fid: 'name', + title: 'Domain Name', + type: 'text', + template: function(row) { + return '' + row.name + ''; + } + },{ + fid: 'port', + title: 'Port', + type: 'text' + },{ + title:'OPT', + type:'group', + width:'100px', + align: 'right', + group:[{ + title: 'Del', + event: function (rowc,index,ev,key,rthat) { + that.remove_node_project_domain({project_name:row.name,domain:(rowc.name+':'+rowc.port)},function(res){ + bt.msg({status:res.status,msg:res.data || res.error_msg}) + rthat.$refresh_table_list(true); + }) + } + }] + }], + tootls: [{ // 批量操作 + type: 'batch', + positon: ['left', 'bottom'], + placeholder: 'Please select bulk operation', + buttonValue: 'Batch operation', + disabledSelectValue: 'Please select the site that needs batch operation!', + selectList: [{ + title: "Delete domain name", + load: true, + url: '/project/nodejs/project_remove_domain', + param: function (crow) { + return {data:JSON.stringify({project_name: row.name,domain: (crow.name+':'+crow.port)})} + }, + callback: function (that) { // 手动执行,data参数包含所有选中的站点 + bt.show_confirm("Delete domain names in bulk", "Delete the selected domain name at the same time, do you want to continue?", function () { + var param = {}; + that.start_batch(param, function (list) { + var html = '' + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += '' + item.name + '
      ' + (item.request.status?'Success':'Fail') + '
      '; + } + project_domian.$batch_success_table({ + title: 'Batch deletion', + th: 'Delete domain name', + html: html + }); + project_domian.$refresh_table_list(true); + }); + }); + } + }] + }] + }) + setTimeout(function(){ + + $(el).append('') + + },100) + }, + + /** + * @description 渲染Node项目映射 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_map:function(el,row){ + var that = this; + el.html('
      ' + + ' Mapping' + + '
      ' + + ' ' + + ' ' + + '
      ' + + '
      ') + $('#node_project_map').attr('checked',row['project_config']['bind_extranet']?true:false) + $('[name=node_project_map]').click(function(){ + var _check = $('#node_project_map').prop('checked'),param = {project_name:row.name}; + if(!_check) param['domains'] = row['project_config']['domains'] + layer.confirm((!_check?'Enable':'Disable')+' mapping!,do you want to continue?',{ + title:'Mapping', + icon:0, + closeBtn:2, + cancel:function(){ + $('#node_project_map').attr('checked',_check) + } + },function(){ + that[_check?'unbind_node_project_map':'bind_node_project_map'](param,function(res){ + if(!res.status) $('#node_project_map').attr('checked',_check) + bt.msg({status:res.status,msg:typeof res.data != "string"?res.error_msg:res.data}) + row['project_config']['bind_extranet'] = _check?0:1 + }) + },function(){ + $('#node_project_map').attr('checked',_check) + }) + }) + }, + + /** + * @description 渲染Node项目模块 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_module:function(el,row){ + var that = this; + el.html('
      ' + + '
      ' + + '
      ' + + '
      '); + var node_project_module_table = bt_tools.table({ + el:'#node_module_list', + url:'/project/nodejs/get_project_modules', + default: "The module is not installed, click one-click to install the project module, the default prompt when the data is empty", + param:{project_name:row.name,project_cwd:row.path}, + height:'576px', + load:'Retrieving module list, please wait...', + beforeRequest:function(params){ + if(params.hasOwnProperty('data') && typeof params.data === 'string') return params + return {data:JSON.stringify(params)} + }, + column:[{ + fid: 'name', + title: 'Module', + type: 'text', + },{ + fid: 'version', + title: 'Ver', + type: 'text', + width:'60px' + },{ + fid: 'license', + title: 'License', + type: 'text', + template:function(row){ + if(typeof row.license === "object") return ''+row.license.type+''; + return ''+row.license+'' + } + },{ + fid: 'description', + title: 'Description', + width:235, + type: 'text', + template:function(row){ + return ''+row.description + '' + } + },{ + title:'OPT', + type:'group', + width:'125px', + align: 'right', + group:[{ + title: 'Update', + event: function (rowc,index,ev,key,rthat) { + bt.show_confirm("Update module", "Updating the ["+rowc.name+"] module may affect the operation of the project, continue?", function () { + that.upgrade_node_module({project_name:row.name,mod_name:rowc.name},function(res){ + bt.msg({status:res.status,msg:res.data || res.error_msg}) + rthat.$refresh_table_list(true); + }); + }); + } + },{ + title: 'Uninstall', + event: function (rowc,index,ev,key,rthat) { + bt.show_confirm("Uninstall the module", "Uninstalling the ["+rowc.name+"] module may affect the operation of the project, continue?", function () { + that.uninstall_node_module({project_name:row.name,mod_name:rowc.name},function(res){ + bt.msg({status:res.status,msg:res.data || res.error_msg}) + rthat.$refresh_table_list(true); + }); + }); + } + }] + }], + success:function(config){ + // 隐藏一键安装 + if(config.data.length > 0) $('.npm_install_node_config').addClass('hide'); + } + }) + //安装模块 + $('.install_node_module').on('click',function(){ + var _mname = $('input[name=mname]').val(); + if(!_mname) return layer.msg('Please enter the module name and version',{icon:2}) + that.npm_install_node_module({project_name:row.name,mod_name:_mname},function(res){ + bt.msg({status:res.status,msg:res.data || res.error_msg}) + node_project_module_table.$refresh_table_list(true); + }) + }) + //一键安装项目模块 + $('.npm_install_node_config').on('click',function(){ + var _command = that.request_module_log_command({shell:'tail -f /www/server/panel/logs/npm-exec.log'}) + that.install_node_project_packages({project_name:row.name},function(res){ + if(res.status){ + node_project_module_table.$refresh_table_list(true); + } + layer.close(_command); + bt.msg({status: res.status, msg: res.data || res.error_msg}) + }) + }) + }, + + /** + * @description 渲染Node项目伪静态 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_rewrite:function(el,row){ + el.empty(); + if(row.project_config.bind_extranet === 0){ + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show() + return false; + } + site.edit.get_rewrite_list({name:'node_'+row.name},function(){ + $('.webedit-box .line:first').remove(); + $('[name=btn_save_to]').remove(); + $('.webedit-box .help-info-text li:first').remove(); + }) + }, + /** + * @description 渲染Node配置文件 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_file_config:function(el,row){ + el.empty(); + if(row.project_config.bind_extranet === 0){ + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show() + return false; + } + site.edit.set_config({name:'node_'+row.name}) + }, + /** + * @description 渲染node项目使用情况 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_service_condition:function(el,row){ + if(!row.run){ + el.html('').next().removeClass('hide') + if(el.next().find('.node_mask_module_text').length === 1) { + el.next().find('.node_mask_module_text').hide().parent().append('
      Please start the service first and try again,Set service status
      ') + }else{ + el.next().find('.node_mask_module_text:eq(1)').show().prev().hide() + } + return false + } + el.html('
      PID
      ') + var _option = '',tabelCon = '' + for(var load in row.load_info){ + if(row.load_info.hasOwnProperty(load)){ + _option +=''; + } + } + var node_pid = $('[name=node_project_pid]'); + node_pid.html(_option); + node_pid.change(function(){ + var _pid = $(this).val(),rdata = row['load_info'][_pid],fileBody = '',connectionsBody = ''; + for(var i=0;i'+itemi['path']+'' + + ''+itemi['mode']+'' + + ''+itemi['position']+'' + + ''+itemi['flags']+'' + + ''+itemi['fd']+'' + + ''; + } + for(var k=0;k'+itemk['client_addr']+'' + + ''+itemk['client_rport']+'' + + ''+itemk['family']+'' + + ''+itemk['fd']+'' + + ''+itemk['local_addr']+'' + + ''+itemk['local_port']+'' + + ''+itemk['status']+'' + + '' + } + + // tabelCon = reand_table_config([ + // [{"名称":rdata.name},{"PID":rdata.pid},{"状态":rdata.status},{"父进程":rdata.ppid}], + // [{"用户":rdata.user},{"Socket":rdata.connects},{"CPU":rdata.cpu_percent},{"线程":rdata.threads}], + // [{"内存":rdata.user},{"io读":rdata.connects},{"io写":rdata.cpu_percent},{"启动时间":rdata.threads}], + // [{"启动命令":rdata.user}], + // ]) + // + // console.log(tabelCon) + // + // + // function reand_table_config(conifg){ + // var html = ''; + // for (var i = 0; i < conifg.length; i++) { + // var item = conifg[i]; + // html += ''; + // for (var j = 0; j < item; j++) { + // var items = config[j],name = Object.keys(items)[0]; + // console.log(items,name) + // html += ''+ name +''+ items[name] +'' + // } + // console.log(html) + // html += '' + // } + // return '
      '+ html +'
      '; + // } + + + tabelCon = '
      ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '
      Name' + rdata.name + 'Status' + rdata.status + 'User' + rdata.user + 'Start Time' + getLocalTime(rdata.create_time) + '
      PID' + rdata.pid + 'PPID' + rdata.ppid + 'Thread' + rdata.threads + 'Socket' + rdata.connects + '
      CPU' + rdata.cpu_percent + '%RAM' + ToSize(rdata.memory_used) + 'Disk/R' + ToSize(rdata.io_read_bytes) + 'Dis/W' + ToSize(rdata.io_write_bytes) + '
      Command' + rdata.exe + '
      ' + + '
      ' + + '

      Network

      ' + + '
      ' + + '
      ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + connectionsBody + '' + + '
      Client addressClient portProtocolFDlocal addresslocal portStatus
      ' + + '
      ' + + '
      ' + + '

      Open files

      ' + + '
      ' + + '
      ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + fileBody + '' + + '
      Filesmodepositionflagsfd
      ' + + '
      ' + + '
      ' + $('.node_project_pid_datail').html(tabelCon); + bt_tools.$fixed_table_thead('#nodeNetworkList') + bt_tools.$fixed_table_thead('#nodeFileList') + }).change().html(_option); + }, + + /** + * @description 渲染Node项目日志 + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_log:function(el,row){ + el.html('
      ') + bt_tools.send({ + url:'/project/nodejs/get_project_log', + type:'GET', + data:{data:JSON.stringify({project_name: row.name})} + },function(res){ + $('#webedit-con .node_project_log').html('
      '+(typeof res == "object"?res.error_msg:res)+'
      ') + $('.command_output_pre').scrollTop($('.command_output_pre').prop('scrollHeight')) + },{load:'Get Node project log',verify:false}) + }, + + reander_node_site_log:function(el,row){ + el.empty() + if(row.project_config.bind_extranet === 0){ + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show() + return false; + } + site.edit.get_site_logs({name:row.name}) + }, + + /** + * @description node项目SSL + * @param el {object} 当前element节点 + * @param row {object} 当前项目数据 + */ + reander_node_project_ssl:function(el,row){ + el.empty(); + if(row.project_config.bind_extranet === 0){ + $('.mask_module').removeClass('hide').find('.node_mask_module_text:eq(1)').hide().prev().show() + return false; + } + site.set_ssl({name:row.name,ele:el,id:row.id}); + site.ssl.reload(); + }, + /** + * @description 请求模块日志终端 + * @param config {object} 当前配置数据 + */ + request_module_log_command:function(config){ + var r_command = layer.open({ + title:config.name || 'Module is being installed, please wait...', + type: 1, + closeBtn: 0, + area:['500px','342px'], + skin:config.class || 'module_commmand', + shadeClose: false, + content:'
      ', + success:function(){ + bt_tools.command_line_output({ el:'.site_module_command', shell:config.shell,area:config.area||['100%','300px']}) + } + }) + return r_command; + }, + + /** + * @description 请求封装 + * @param keyMethod 接口名和loading,键值对 + * @param param {object || function} 参数,可为空,为空则为callback参数 + * @param callback {function} 成功回调函数 + * @param callback1 {function} 错误调函数 + */ + http:function(keyMethod,param,callback,callback1){ + var method = Object.keys(keyMethod), + config = { + url: '/project/nodejs/' + method[0], + data: param && {data: JSON.stringify(param)} || {} + }, + success = function (res) { + callback && callback(res) + } + if (callback1) { + bt_tools.send(config, success, callback1, {load: keyMethod[method[0]],verify:method[1]?keyMethod[method[1]]:true}) + } else { + bt_tools.send(config,success,{load: keyMethod[method[0]],verify:method[1]?keyMethod[method[1]]:true}) + } + } + }, + node_porject_view: function(){ + var node_table = bt_tools.table({ + el: '#bt_node_table', + url: '/project/nodejs/get_project_list', + minWidth: '1000px', + autoHeight: true, + default: "The item list is empty", //数据为空时的默认提示\ + load:'Getting the list of Node projects, please wait...', + beforeRequest:function(params){ + if(params.hasOwnProperty('data') && typeof params.data === 'string') { + var oldParams = JSON.parse(params['data']) + delete params['data'] + return {data:JSON.stringify($.extend(oldParams,params))} + } + return {data:JSON.stringify(params)} + }, + column: [ + { type: 'checkbox', class: '', width: 20 }, + { + fid: 'name', + title: 'Name', + width: 85, + type: 'link', + event: function(row, index, ev) { + site.node.set_node_project_view(row); + } + }, + { + fid: 'run', + title: 'Status', + width: 85, + config: { + icon: true, + list: [ + [true, 'Running', 'bt_success', 'glyphicon-play'], + [false, 'Stop', 'bt_danger', 'glyphicon-pause'] + ] + }, + type: 'status', + event: function(row, index, ev, key, that) { + var status = row.run; + bt.confirm({ + title:status?'Stop project':'Startup project', + msg:status?'After stopping the project, the project service will stop running, continue?':'Startup Node project ['+ row.name +'], continue operation?' + },function(index){ + layer.close(index) + site.node[status?'stop_node_project':'start_node_project']({project_name:row.name},function(res){ + bt.msg({status:res.status,msg:res.data || res.error_msg}) + that.$refresh_table_list(true); + }) + }) + } + }, + { + fid:'pid', + title:'PID', + width:180, + type:'text', + template:function(row){ + if($.isEmptyObject(row['load_info'])) return '-' + var _id = [] + for(var i in row.load_info){ + if(row.load_info.hasOwnProperty(i)){ + _id.push(i) + } + } + return ''+_id.join(',')+'' + } + }, + { + title:'CPU', + type:'text', + template:function(row){ + if($.isEmptyObject(row['load_info'])) return '-' + var _cpu_total = 0; + for(var i in row.load_info){ + _cpu_total += row.load_info[i]['cpu_percent'] + } + return ''+ _cpu_total.toFixed(2) +'%' + } + }, + { + title:'RAM', + type:'text', + template:function(row){ + if($.isEmptyObject(row['load_info'])) return '-' + var _cpu_total = 0; + for(var i in row.load_info){ + _cpu_total+=row.load_info[i]['memory_used'] + } + return ''+bt.format_size(_cpu_total)+'' + } + }, + { + fid: 'path', + title: 'Root directory', + tips: 'Open Directory', + type: 'link', + event: function(row, index, ev) { + openPath(row.path); + } + }, + { + fid:'node_version', + title:'Node version', + type:'text', + template:function(row){ + return ''+row['project_config']['nodejs_version']+'' + } + },{ + fid: 'ps', + title: 'Remark', + type: 'input', + blur: function(row, index, ev, key, that) { + if (row.ps == ev.target.value) return false; + bt.pub.set_data_ps({ id: row.id, table: 'sites', ps: ev.target.value }, function(res) { + bt_tools.msg(res, { is_dynamic: true }); + }); + }, + keyup: function(row, index, ev) { + if (ev.keyCode === 13) { + $(this).blur(); + } + } + },{ + fid: 'ssl', + title: 'SSL', + tips: 'Deployment certificate', + width: 100, + type: 'text', + template: function(row, index) { + var _ssl = row.ssl, + _info = '', + _arry = [ + ['issuer', 'issuer'], + ['notAfter', 'Due date'], + ['notBefore', 'Application date'], + ['dns', 'Available domain names'] + ]; + try { + if (typeof row.ssl.endtime != 'undefined') { + if (row.ssl.endtime < 0) { + return 'Exp in ' + Math.row.ssl.endtime + ' days'; + } + } + } catch (error) {} + for (var i = 0; i < _arry.length; i++) { + var item = _ssl[_arry[i][0]]; + _info += _arry[i][1] + ':' + item + (_arry.length - 1 != i ? '\n' : ''); + } + return row.ssl === -1 ? 'Not Set' : 'Exp in ' + row.ssl.endtime + ' days'; + }, + event: function(row) { + site.node.set_node_project_view(row); + setTimeout(function() { + $('.site-menu p:eq(5)').click(); + }, 500); + } + },{ + title: 'OPT', + type: 'group', + width: 100, + align: 'right', + group: [{ + title: 'Set', + event: function(row, index, ev, key, that) { + site.node.set_node_project_view(row); + } + }, { + title: 'Del', + event: function(row, index, ev, key, that) { + bt.prompt_confirm('Delete item','You are deleting the Node project-['+ row.name +'], continue?',function(){ + site.node.remove_node_project({project_name:row.name},function(res){ + bt.msg({status:res.status,msg:res.data || res.error_msg}) + node_table.$refresh_table_list(true); + }) + }) + } + }] + } + ], + sortParam: function(data) { + return { 'order': data.name + ' ' + data.sort }; + }, + // 渲染完成 + tootls: [{ // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [{ + title: 'Add Node project', + active: true, + event: function(ev) { + site.node.add_node_form(function(res,index){ + if(res.status) { + layer.close(index) + node_table.$refresh_table_list(true); + } + bt.msg({status:res.status, msg: (!Array.isArray(res.data)?res.data:false) || res.error_msg}) + }) + } + },{ + title: 'Node version manager', + event: function(ev) { + bt.soft.set_lib_config('nodejs','Node.js version manager') + } + }] + }, { // 搜索内容 + type: 'search', + positon: ['right', 'top'], + placeholder: 'Please enter the project name', + searchParam: 'search', //搜索请求字段,默认为 search + value: '', // 当前内容,默认为空 + }, { // 批量操作 + type: 'batch', //batch_btn + positon: ['left', 'bottom'], + placeholder: 'Please select bulk operation', + buttonValue: 'Batch operation', + disabledSelectValue: 'Please select the site that needs batch operation!', + selectList: [{ + title: "Delete item", + url: '/project/nodejs/remove_project', + param:function(row){ + return { + data:JSON.stringify({project_name:row.name}) + } + }, + refresh: true, + callback: function(that) { + bt.prompt_confirm('Delete items in bulk','You are deleting the selected Node project. Continue?',function(){ + that.start_batch({}, function(list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += '' + item.name + '
      ' + (item.requests.status?item.requests.data:item.requests.error_msg) + '
      '; + } + node_table.$batch_success_table({ title: 'Delete items in bulk', th: 'project name', html: html }); + node_table.$refresh_table_list(true); + }); + }) + } + }], + }, { //分页显示 + type: 'page', + positon: ['right', 'bottom'], // 默认在右下角 + pageParam: 'p', //分页请求字段,默认为 : p + page: 1, //当前分页 默认:1 + numberParam: 'limit', + //分页数量请求字段默认为 : limit + number: 20, + //分页数量默认 : 20条 + numberList: [10, 20, 50, 100, 200], // 分页显示数量列表 + numberStatus: true, // 是否支持分页数量选择,默认禁用 + jump: true, //是否支持跳转分页,默认禁用 + }] + }); + }, + php_table_view:function(){ + var site_table = bt_tools.table({ + el:'#bt_site_table', + url:'/data?action=getData', + cookiePrefix:'site_table', // cookie前缀,用于状态存储,如果不设置,着所有状态不存储, + param:{table:'sites'}, //参数 + minWidth:'1000px', + autoHeight:true, + default:"Site list is empty", // 数据为空时的默认提示 + beforeRequest:function(param){ + param.type = bt.get_cookie('site_type') || -1; + return param; + }, + column:[ + {type:'checkbox',class:'',width:20}, + {fid:'name',title:lan.site.site_name,sort:true,sortValue:'asc',type:'link',event:function(row,index,ev){ + site.web_edit(row,true); + }}, + {fid:'status',title:lan.site.status,sort:true,width:85,config:{icon:true,list:[['1',lan.site.running_text,'bt_success','glyphicon-play'],['0',lan.site.stopped,'bt_danger','glyphicon-pause']]},type:'status',event:function(row,index,ev,key,that){ + bt.site[parseInt(row.status)?'stop':'start'](row.id,row.name,function(res){ + if(res.status) that.$modify_row_data({status:parseInt(row.status)?'0':'1'}); + }); + }}, + { + fid:'backup_count', + title:lan.site.backup, + width:80, + type:'link', + template:function(row,index){ + var backup = lan.site.backup_no,_class = "bt_warning"; + if (row.backup_count > 0) backup = lan.site.backup_yes,_class = "bt_success"; + return ''+ backup + (row.backup_count >0?('('+ row.backup_count +')'):'') +''; + }, + event:function(row,index){ + site.backup_site_view({id:row.id,name:row.name},site_table); + } + }, + {fid:'path',title:lan.site.root_dir,tips:'Open path',type:'link',event:function(row,index,ev){ + openPath(row.path); + }}, + { + fid:'edate', + title: lan.site.endtime, + width: 115, + class: 'set_site_edate', + sort: true, + type: 'link', + template: function (row,index) { + var _endtime = row.edate || row.endtime; + if (_endtime === "0000-00-00") { + return lan.site.web_end_time; + } else { + if (new Date(_endtime).getTime() < new Date().getTime()) { + return '' + _endtime + ''; + } else { + return _endtime; + } + } + }, + event: function (row) {} + }, //模拟点击误删 + {fid:'ps',title:lan.site.note,type:'input',blur:function(row,index,ev){ + if (row.ps == ev.target.value) return false; + bt.pub.set_data_ps({id:row.id,table:'sites',ps:ev.target.value},function(res){ + if(!res.status) layer.msg(res.msg,{status:2}); + }); + },keyup:function(row,index,ev){ + if(ev.keyCode === 13){ + $(this).blur(); + } + }}, + {fid:'php_version',title:'PHP',tips:'Selete php version',width:57,type:'link',template:function(row,index){ + if(row.php_version.indexOf('static') > -1) return row.php_version; + return row.php_version; + },event:function(row,index){ + site.web_edit(row); + setTimeout(function(){ + $('.site-menu p:eq(9)').click(); + },500); + }}, + {fid:'ssl',title:'SSL',tips:'Deployment certificate',width:140,type:'text',template:function(row,index){ + var _ssl = row.ssl,_info = '',_arry = [['issuer','Certificate'],['notAfter','Due date'],['notBefore','Application date'],['dns','Domain name']]; + try { + if (typeof row.ssl.endtime != 'undefined') { + if (row.ssl.endtime < 0) { + return 'Exp in ' + Math.row.ssl.endtime + ' days'; + } + } + } catch (error) {} + for (var i = 0; i < _arry.length; i++) { + var item = _ssl[_arry[i][0]]; + _info += _arry[i][1] + ':' + item + (_arry.length - 1 != i ? '\n' : ''); + } + return row.ssl === -1 ? 'Not Set' : 'Exp in ' + row.ssl.endtime + ' days'; + },event:function(row,index,ev,key,that){ + console.log(row,'111'); + site.web_edit(row); + setTimeout(function(){ + $('.site-menu p:eq(8)').click(); + },500); + }}, + {title:lan.site.operate,type:'group',width:119,align:'right',group:[ + { + title:'WAF', + event:function(row,index,ev,key,that){ + site.site_waf(row.name); + } + }, + { + title:lan.site.set, + event:function(row,index,ev,key,that){ + site.web_edit(row,true); + } + },{ + title:'Del', + event:function(row,index,ev,key,that){ + site.del_site(row.id,row.name,function(){ + that.$refresh_table_list(true); + }); + } + }]} + ], + sortParam:function(data){ + return {'order':data.name +' '+ data.sort}; + }, + // 表格渲染完成后 + success: function (that) { + $('.event_edate_' + that.random).each(function(){ + var $this = $(this); + laydate.render({ + elem: $this[0] //指定元素 + , min: bt.get_date(1) + , max: '2099-12-31' + , vlue: bt.get_date(365) + , type: 'date' + , format: 'yyyy-MM-dd' + , trigger: 'click' + , btns: ['perpetual', 'confirm'] + , theme: '#20a53a' + , ready:function(){ + $this.click(); + } + , done: function (date) { + var item = that.event_rows_model.rows; + bt.site.set_endtime(item.id, date,function(res){ + if(res.status){ + layer.msg(res.msg); + return false; + } + bt.msg(res); + }); + } + }); + }); + }, + // 渲染完成 + tootls:[{ // 按钮组 + type:'group', + positon:['left','top'], + list:[ + {title:'Add site',active:true, event:function(ev){ site.add_site(function(){ + site_table.$refresh_table_list(true) }); + bt.set_cookie('site_type','-1'); + }}, + {title:'Default Page',event:function(ev){ site.set_default_page() }}, + {title:'Default Website',event:function(ev){ site.set_default_site() }}, + {title:'PHP CLI version',event:function(ev){ site.get_cli_version()}} + ] + },{ // 搜索内容 + type:'search', + positon:['right','top'], + placeholder:'Domain or Remarks', + searchParam:'search', //搜索请求字段,默认为 search + value:'',// 当前内容,默认为空 + },{ // 批量操作 + type:'batch',//batch_btn + positon:['left','bottom'], + placeholder:'Select batch operation', + buttonValue:'Execute', + disabledSelectValue:'Select the website to execute!', + selectList:[ + { + group:[{title:lan.site.enable_website,param:{status:1}},{title:'Disable website',param:{status:0}}], + url:'/site?action=set_site_status_multiple', + confirmVerify:false, //是否提示验证方式 + paramName:'sites_id', //列表参数名,可以为空 + paramId:'id', // 需要传入批量的id + theadName:'Name' + },{ + title:lan.site.backup_website, + url:'/site?action=ToBackup', + paramId:'id', + load:true, + theadName:'Name', + callback:function(that){ // 手动执行,data参数包含所有选中的站点 + that.start_batch({},function(list){ + var html = ''; + for(var i=0;i
      '+ item.request.msg +'
      '; + } + site_table.$batch_success_table({title:'Batch backup',th:'Site name',html:html}); + site_table.$refresh_table_list(true); + }); + } + },{ + title:lan.site.set_expired, + url:'/site?action=set_site_etime_multiple', + paramName:'sites_id', //列表参数名,可以为空 + paramId:'id', // 需要传入批量的id + theadName:'Name', + confirm:{ + title:'Batch set expired date', + content:'
      Expired date
      ', + success:function(){ + laydate.render({ + elem: '#site_edate' + ,min: bt.format_data(new Date().getTime(),'yyyy-MM-dd') + ,max: '2099-12-31' + ,vlue: bt.get_date(365) + ,type: 'date' + ,format: 'yyyy-MM-dd' + ,trigger: 'click' + ,btns: ['perpetual','confirm'] + ,theme: '#20a53a' + }); + }, + yes:function(index,layers,request){ + var site_edate = $('#site_edate'),site_edate_val = site_edate.val(); + if(site_edate_val != ''){ + request({'edate':site_edate_val==='Forever'?'0000-00-00':site_edate_val}); + }else{ + layer.tips('Input expired date','#site_edate',{tips:['1','red']}); + $('#site_edate').css('border-color','red'); + $('#site_edate').click(); + setTimeout(function(){ + $('#site_edate').removeAttr('style'); + },3000); + return false; + } + } + } + },{ + title:lan.site.set_php_version, + url:'/site?action=set_site_php_version_multiple', + paramName:'sites_id', //列表参数名,可以为空 + paramId:'id', // 需要传入批量的id + theadName:'Name', + confirm:{ + title:'Batch set php version', + area:'420px', + content:'
      PHP version
      • Please select the version according to your program requirements.
      • If not necessary, please try not to use PHP 5.2, which will reduce your server security.
      • PHP 7 does not support mysql extension, mysqli and mysql_pdo will be installed by default.
      ', + success:function(){ + bt.site.get_all_phpversion(function(res){ + var html = ''; + $.each(res,function(index,item){ + html += ''; + }); + $('[name="versions"]').html(html); + }); + }, + yes:function(index,layers,request){ + request({version:$('[name="versions"]').val()}); + } + } + },{ + title:lan.site.set_category, + url:'/site?action=set_site_type', + paramName:'site_ids', //列表参数名,可以为空 + paramId:'id', // 需要传入批量的id + beforeRequest:function(list){ + var arry = []; + $.each(list,function(index,item){ + arry.push(item.id); + }); + return JSON.stringify(arry); + }, + confirm:{ + title:'Batch set category', + content:'
      Site category
      ', + success:function(){ + bt.site.get_type(function(res){ + var html = ''; + $.each(res,function(index,item){ + html += ''; + }); + $('[name="site_types"]').html(html); + }); + }, + yes:function(index,layers,request){ + request({id:$('[name="site_types"]').val()}); + } + }, + tips:false, + success:function(res,list,that){ + var html = ''; + $.each(list,function(index,item){ + html += ''+ item.name +'
      '+ res.msg +'
      '; + }); + that.$batch_success_table({title:'Batch set category',th:'Site name',html:html}); + that.$refresh_table_list(true); + } + },{ + title:lan.site.del_website, + url:'/site?action=DeleteSite', + // paramName:'sites_id', //列表参数名,可以为空 + // paramId:'id', //需要传入批量的id + // theadName:'Name', + param: function (row) { + return { + id: row.id, + webname: row.name + } + }, + load: true, + callback: function(that){ + // bt.show_confirm("Delete site","Confirm delete the FTP、database、root path of the selected site with the same name", function(){ + // var param = {}; + // $('.bacth_options input[type=checkbox]').each(function(){ + // var checked = $(this).is(":checked"); + // if(checked) param[$(this).attr('name')] = checked?1:0; + // }) + // if(callback) callback(param); + // },"
      "); + var ids = []; + for (var i = 0; i < that.check_list.length; i++) { + ids.push(that.check_list[i].id); + } + site.del_site(ids,function(param){ + that.start_batch(param, function (list) { + layer.closeAll() + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += '' + item.name + '
      ' + item.request.msg + '
      '; + } + site_table.$batch_success_table({ + title: 'Batch delete', + th: 'site name', + html: html + }); + site_table.$refresh_table_list(true); + }); + }); + } + } + ], + },{ //分页显示 + type:'page', + positon:['right','bottom'], // 默认在右下角 + pageParam:'p', //分页请求字段,默认为 : p + page:1, //当前分页 默认:1 + numberParam:'limit', //分页数量请求字段默认为 : limit + number:20, //分页数量默认 : 20条 + numberList:[10,20,50,100,200], // 分页显示数量列表 + numberStatus:true, // 是否支持分页数量选择,默认禁用 + jump:true, //是否支持跳转分页,默认禁用 + }] + }); + $('.tootls_group.tootls_top .pull-left').append('
      Classification:
        '); + bt.site.get_type(function(res) { + site.reader_site_type(res,site_table); + }); + }, + reader_site_type: function(res,config) { + var html = '', + active = bt.get_cookie('site_type') || -1, + select = $('.site_class_type'); + if(select.find('.bt_select_list li').length > 1) return false + res.unshift({ id: -1, name: "Category manager" }); + $.each(res, function(index, item) { + html += '
      • ' + item.name + '
      • '; + }); + html += '
      • Category set
      • '; + select.find('.bt_select_value').on('click', function(ev) { + var $this = this; + $(this).next().show(); + $(document).one('click', function() { + $($this).next().hide(); + }); + ev.stopPropagation() + }); + + select.find('.bt_select_list').unbind('click').on('click', 'li', function() { + var id = $(this).data('id'); + if (id === 'type_sets') { + site.set_class_type(); + } else { + bt.set_cookie('site_type', id); + config.$refresh_table_list(true); + $(this).addClass('active').siblings().removeClass('active'); + // select.find('.bt_select_value .bt_select_content').text('Classification: ' + $(this).text()); + select.find('.bt_select_value .bt_select_content').text($(this).text()); + } + + }).empty().html(html); + select = $(select[0]) + if (!select.find('.bt_select_list li.active').length) { + select.find('.bt_select_list li:eq(0)').addClass('active'); + // select.find('.bt_select_value .bt_select_content').text('Classification: 默认分类'); + select.find('.bt_select_value .bt_select_content').text('Classification: 默认分类'); + } else { + // select.find('.bt_select_value .bt_select_content').text('Classification: ' + select.find('.bt_select_list li.active').text()); + select.find('.bt_select_value .bt_select_content').text(select.find('.bt_select_list li.active').text()); + } + }, get_list: function(page, search, type) { if (page == undefined) page = 1; if (type == '-1' || type == undefined) { @@ -398,7 +1997,7 @@ var site = { }, { field: 'php_version',width:70, title: 'PHP', templet: function (item) { - + return ''+item.php_version+''; } }, @@ -412,11 +2011,11 @@ var site = { var ssl_info = "Certificate: "+item.ssl.issuer+"
        Due date: " + item.ssl.notAfter+"
        Application date: " + item.ssl.notBefore +"
        Domain name: " + item.ssl.dns.join("/"); if(item.ssl.endtime < 0){ _ssl = 'Expired'; - + }else if(item.ssl.endtime < 20){ - _ssl = 'Expire: '+(item.ssl.endtime+' days')+''; + _ssl = 'Exp in '+(item.ssl.endtime+' days')+''; }else{ - _ssl = 'Expire: '+item.ssl.endtime+' days'; + _ssl = 'Exp in '+item.ssl.endtime+' days'; } } return _ssl; @@ -541,7 +2140,7 @@ var site = { site.get_list(0,'', val); $(".site_type button").removeClass('btn-success').addClass('btn-default'); $(this).addClass('btn-success'); - + }) if (callback) callback(rdata); }); @@ -641,6 +2240,152 @@ var site = { }, 100) }); }, + /** + * @description 备份站点视图 + * @param {object} config 配置参数 + * @param {function} callback 回调函数 + */ + backup_site_view: function(config, thatC,callback) { + bt_tools.open({ + title: lan.site.backup_title+' - [ ' + config.name + ' ]', + area: '720px', + btn: false, + skin: 'bt_backup_table', + content: '
        ', + success: function() { + var backup_table = bt_tools.table({ + el: '#bt_backup_table', + url: '/data?action=getData', + param: { table: 'backup', search: config.id, type: '0' }, + default: "[" + config.name + "] Currently no backup", //数据为空时的默认提示 + column: [ + { type: 'checkbox', class: '', width: 20 }, + { fid: 'name', title: lan.site.filename, width: 320, fixed: true }, + { + fid: 'size', + title: lan.site.filesize, + width: 80, + type: 'text', + template: function(row, index) { + return bt.format_size(row.size); + } + }, + { fid: 'addtime', width: 150, title: lan.site.backup_time }, + { + title: lan.site.operate, + type: 'group', + width: 120, + align: 'right', + group: [{ + title:'Restore', + event: function() { + var data = {}; + data.file_name = $(this).attr('backup-name'); + data.site_id = $(this).attr('site-id'); + layer.confirm('Are you sure to restore backup file?', { + icon: 0, + closeBtn: 2, + title: 'Restore backup file', + }, function (index) { + $.post('/files?action=restore_website', data, function(rdata) { + layer.close(index); + site.backup_output_stop = true; + layer.msg(rdata.msg, {icon: rdata.status ? 1 : 2}); + }); + site.backup_output_logs(); + }); + } + },{ + title: lan.site.download, + template: function(row, index, ev, key, that) { + return ''+lan.site.download+''; + } + }, { + title: lan.site.del, + event: function(row, index, ev, key, that) { + that.del_site_backup({ name: row.name, id: row.id }, function(rdata) { + bt_tools.msg(rdata); + if (rdata.status) { + thatC.$modify_row_data({ backup_count: thatC.event_rows_model.rows.backup_count - 1 }); + that.$refresh_table_list(); + } + }); + } + }] + } + ], + methods: { + /** + * @description 删除站点备份 + * @param {object} config + * @param {function} callback + */ + del_site_backup: function(config, callback) { + bt.confirm({ title: lan.site.del_bak_file, msg: 'The website backup is about to be deleted [' + config.name + '], do you want to continue?' }, function() { + bt_tools.send('site/DelBackup', { id: config.id }, function(rdata) { + if (callback) callback(rdata); + }, true); + }); + } + }, + success: function() { + if (callback) callback(); + $('.bt_backup_table').css('top', (($(window).height() - $('.bt_backup_table').height()) / 2) + 'px'); + }, + tootls: [{ // 按钮组 + type: 'group', + positon: ['left', 'top'], + list: [{ + title: 'Backup', + active: true, + event: function(ev, that) { + bt.site.backup_data(config.id, function(rdata) { + bt_tools.msg(rdata); + if (rdata.status) { + thatC.$modify_row_data({ backup_count: thatC.event_rows_model.rows.backup_count + 1 }); + that.$refresh_table_list(); + } + }); + } + }] + }, { + type: 'batch', + positon: ['left', 'bottom'], + config: { + title: ' Delete', + url: '/site?action=DelBackup', + paramId: 'id', + load: true, + callback: function(that) { + bt.confirm({ title: 'Delete site backups in bulk', msg: 'Do you want to delete selected site backups in batches?', icon: 0 }, function(index) { + layer.close(index); + that.start_batch({}, function(list) { + var html = ''; + for (var i = 0; i < list.length; i++) { + var item = list[i]; + html += '' + item.name + '
        ' + item.request.msg + '
        '; + } + backup_table.$batch_success_table({ title: 'Delete site backups in bulk', th: 'file name', html: html }); + backup_table.$refresh_table_list(true); + thatC.$modify_row_data({ backup_count: thatC.event_rows_model.rows.backup_count - list.length }); + }); + }); + } + } //分页显示 + }, { + type: 'page', + positon: ['right', 'bottom'], // 默认在右下角 + pageParam: 'p', //分页请求字段,默认为 : p + page: 1, //当前分页 默认:1 + numberParam: 'limit', +   //分页数量请求字段默认为 : limit + number: 10, +   //分页数量默认 : 20条 + }] + }); + } + }); + }, backup_output_stop: false, //实时显示过程 backup_output_logs: function () { @@ -908,18 +2653,20 @@ var site = { group:[{ type:'checkbox', name:'set_ssl', - label_tips:'Apply for SSL', - event:function(value,form,that,config,ev){ - var set_ssl = $(this).is(':checked'); - if(!set_ssl) $('input[name=set_ssl],input[name=force_ssl]').prop('checked',set_ssl); - } + title:'Apply for SSL', + class:'site_ssl_check', + style:{'margin-right': '10px'} },{ type:'checkbox', name:'force_ssl', - label_tips:'HTTP redirect to HTTPS', + class:'site_ssl_check', + title:'HTTP redirect to HTTPS', event:function(value,form,that,config,ev){ var force_ssl = $(this).is(':checked'); - if(force_ssl) $('input[name=set_ssl]').prop('checked',force_ssl); + if(force_ssl){ + $('.site_ssl_check:eq(0)').find('i').addClass('active'); + $('input[name=set_ssl]').prop('checked',force_ssl); + } } }] } @@ -974,7 +2721,7 @@ var site = { } }], success:function(){ - + } }); bt_tools.open({ @@ -987,9 +2734,8 @@ var site = { }, yes:function(indexs){ var formValue = !web_tab.active?add_web.$get_form_value():bath_web.$get_form_value(); - console.log(formValue) if(formValue.webname === ''){ - bt.msg({status:false,msg:'The website domain name cannot be empty!'}) + bt.msg({status:false,msg:'网站域名不能为空!'}) return false; } if(!web_tab.active){ // 创建站点 @@ -1019,12 +2765,12 @@ var site = { return false; } Array.isArray(item)? item = item[0]:''; - if(formValue['ftp'] === 'false' && (item === 'ftp_username' || item === 'ftp_password')) return true; + if(formValue['ftp'] === 'false' && (item === 'ftp_username' || item === 'ftp_password')) return true; if(formValue['sql'] === 'false' && (item === 'datauser' || item === 'datapassword')) return true; param[item] = formValue[item]; }); - param['set_ssl'] = $('input[name=set_ssl]').is(':checked')?1:0; - param['force_ssl'] = $('input[name=force_ssl]').is(':checked')?1:0; + param['set_ssl'] = $('input[name=set_ssl]').prop('checked')?1:0; + param['force_ssl'] = $('input[name=force_ssl]').prop('checked')?1:0; var is_redirect = $('.redirect_check').hasClass('hide'); if(!is_redirect){ var redirect_check = $('.redirect_check input[name=redirect]').is(':checked'); @@ -1072,7 +2818,7 @@ var site = { shadeClose: false, content: "
        " + ftpData + sqlData + "
        " }); - + if ($(".success-msg").height() < 150) { $(".success-msg").find("img").css({ "width": "150px", "margin-top": "30px" }); } @@ -1248,22 +2994,188 @@ var site = { }); }, del_site: function(wid, wname,callback) { - var thtml = "
        "; - bt.show_confirm(lan.site.site_del_title + "[" + wname + "]", lan.site.site_del_info, function() { - var ftp = '', - data = '', - path = '', - data = { id: wid, webname: wname } - if ($("#delftp").is(":checked")) data.ftp = 1; - if ($("#deldata").is(":checked")) data.database = 1; - if ($("#delpath").is(":checked")) data.path = 1; + var num1 = bt.get_random_num(1, 9), + num2 = bt.get_random_num(1, 9), + title = ''; + title = typeof wname === "function" ? 'Deleting sites in batches' : lan.site.site_del_title + ' [ '+ wname +' ]'; + layer.open({ + type: 1, + title: title, + icon: 0, + skin: 'delete_site_layer', + area: '480px', + closeBtn: 2, + shadeClose: true, + content: '\ +
        \ + \ +
        ' + lan.site.site_del_info + '
        \ +
        \ + \ + \ + \ +
        \ +
        \ + ' + lan.bt.cal_msg + '' + num1 + ' + ' + num2 + '=\ + \ +
        \ +
        \ + ', + btn:[lan.public.ok, lan.public.cancel], + success: function (layers, indexs) { + $(layers).find('.check_type_group label').hover(function(){ + var name = $(this).find('input').attr('name'); + if (name === 'data' && !recycle_bin_db_open) { + layer.tips('Risky operation: the current database recycle bin is not open, delete the database will disappear forever!', this, {tips: [1, 'red'], time: 0}); + } else if (name === 'path' && !recycle_bin_open) { + layer.tips('Risky operation: The current file recycle bin is not open, delete the site directory will disappear forever!', this, {tips: [1, 'red'], time: 0 }); + } + }, function() { + layer.closeAll('tips'); + }); + }, + yes: function (indexs) { + var vcodeResult = $('#vcodeResult'),data = {id: wid,webname: wname}; + $('#site_delete_form input[type=checkbox]').each(function(index,item){ + if($(item).is(':checked')) data[$(item).attr('name')] = 1 + }) + if(vcodeResult.val() === ''){ + layer.tips('The result cannot be null', vcodeResult, {tips: [1, 'red'],time:3000}) + vcodeResult.focus() + return false; + }else if(parseInt(vcodeResult.val()) !== (num1 + num2)){ + layer.tips('The calculation is incorrect', vcodeResult, {tips: [1, 'red'],time:3000}) + vcodeResult.focus() + return false; + } + var is_database = data.hasOwnProperty('database'),is_path = data.hasOwnProperty('path'),is_ftp = data.hasOwnProperty('ftp'); + if((!is_database && !is_path) && (!is_ftp || is_ftp)){ + if(typeof wname === "function"){ + wname(data) + return false; + } + bt.site.del_site(data, function (rdata) { + layer.close(indexs); + if (callback) callback(rdata); + bt.msg(rdata); + }) + return false + } + if(typeof wname === "function"){ + delete data.id; + delete data.webname; + } + layer.close(indexs) + var ids = JSON.stringify(wid instanceof Array ? wid : [ wid ]),countDown = typeof wname === 'string'?4:9; + title = typeof wname === "function" ?'Verify the information twice and delete sites in batches':'Verify information twice, delete site [ ' + wname + ' ]'; + var loadT = bt.load('Checking site data information, please wait...') + bt.send('check_del_data', 'site/check_del_data', {ids: ids}, function (res) { + loadT.close() + layer.open({ + type:1, + title:title, + closeBtn: 2, + skin: 'verify_site_layer_info active', + area: '740px', + content: '
        ' + + '' + + '
        Please calm down for a few seconds and confirm the following data to be deleted.
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        Risks: The database recycle bin function is not enabled at present. After the database is deleted, the database will disappear forever!
        ' + + '
        Risk: The file recycle bin function is disabled at present. After a site directory is deleted, the site directory will disappear forever!
        ' + + '
        Please read the above information to be deleted carefully to prevent site data from being deleted by mistake. Confirm that there are still ' + countDown + ' seconds left to delete.
        ' + + '
        ', + btn: ['Confirm deletion (continue operation after ' + countDown + 'seconds)', 'undelete'], + success: function (layers) { + var html = '', rdata = res.data; + for (var i = 0; i < rdata.length; i++) { + var item = rdata[i], newTime = parseInt(new Date().getTime() / 1000), + t_icon = ''; - bt.site.del_site(data, function(rdata) { - if(rdata.status) callback?callback(rdata):site.get_list(); - bt.msg(rdata); - }); + site_html = (function(item){ + if(!is_path) return '' + var is_time_rule = (newTime - item.st_time) > (86400 * 30) && (item.total > 1024 * 10), + is_path_rule = res.file_size <= item.total, + dir_time = bt.format_data(item.st_time, 'yyyy-MM-dd'), + dir_size = bt.format_size(item.total); - }, thtml); + var f_html = ' ' +(item.limit ? 'More than 50 MB' : dir_size) + ' ' + (is_path_rule ? t_icon : ''); + var f_title = (is_path_rule ?'Note: This directory may contain important data. Exercise caution when performing this operation.\n':'') + 'directory:' + item.path + '(' + (item.limit ? 'greater than ' : '') + dir_size + ')'; + + return '
        ' + + 'site name:' + item.name + '' + + 'directory:' + item.path + ' (' + f_html + ')' + + 'creation time:' + dir_time + '' + + '
        ' + }(item)), + database_html = (function(item){ + if(!is_database || !item.database) return ''; + var is_time_rule = (newTime - item.st_time) > (86400 * 30) && (item.total > 1024 * 10), + is_database_rule = res.db_size <= item.database.total, + database_time = bt.format_data(item.database.st_time, 'yyyy-MM-dd'), + database_size = bt.format_size(item.database.total); + + var f_size = ' ' + database_size + ' ' + (is_database_rule ? t_icon : ''); + var t_size = 'Note: This database is large and may contain important data. Exercise caution when performing this operation.\ndatabase:' + database_size; + + return '
        ' + + 'database:' + item.database.name + '' + + 'size:' + f_size +'' + + 'creation time:' + database_time + '' + + '
        ' + }(item)) + if((site_html + database_html) !== '') html += '
        ' + site_html + database_html +'
        '; + } + if(html === '') html = '
        No data
        ' + $('.check_layer_content').html(html) + var interVal = setInterval(function () { + countDown--; + $(layers).find('.layui-layer-btn0').text('Confirm deletion (continue operation after ' + countDown + ' seconds)') + $(layers).find('.check_layer_message span').text(countDown) + }, 1000); + setTimeout(function () { + $(layers).find('.layui-layer-btn0').text('Confirm the deletion'); + $(layers).find('.check_layer_message').html('Note: please read the above information carefully to prevent site data from being deleted by mistake') + $(layers).removeClass('active'); + clearInterval(interVal) + }, countDown * 1000) + }, + yes:function(indes,layers){ + if($(layers).hasClass('active')){ + layer.tips('Please confirm the information and try again later. ' + countDown + ' seconds left', $(layers).find('.layui-layer-btn0') , {tips: [1, 'red'],time:3000}) + return; + } + if(typeof wname === "function"){ + wname(data) + }else{ + bt.site.del_site(data, function (rdata) { + layer.closeAll() + if (rdata.status) site.get_list(); + if (callback) callback(rdata); + bt.msg(rdata); + }) + } + } + }) + }) + } + }); }, batch_site: function(type, obj, result) { if (obj == undefined) { @@ -1754,7 +3666,7 @@ var site = { setTimeout(function(){ $('#ssl_tabs span:eq('+ ssl_id +')').click(); },1000) - } + } if (action == 'CloseSSLConf') { layer.msg(lan.site.ssl_close_info, { icon: 1, time: 5000 }); } @@ -1963,7 +3875,7 @@ var site = { $(this).hide(); $('.newdomain').focus(); }).css({ 'width':'340px', 'heigth':'100px','left': '0px', 'top': '0px', 'padding-top': '10px','padding-left': '15px'}) - $('.newdomain').focus(function(){ + $('.newdomain').focus(function(){ placeholder.hide(); console.log(placeholder) loadT = layer.tips(placeholder.html(),$(this),{tips:[1,'#20a53a'],time:0,area:$(this).width()}); @@ -2102,7 +4014,7 @@ var site = { }else{ arrs.push({ title: ret.rlist[i], value: ret.rlist[i] }); } - } + } var datas = [{ name: 'dir_rewrite', type: 'select', width: '130px', items: arrs, callback: function (obj) { get_rewrite_file(obj.val()); @@ -2331,7 +4243,7 @@ var site = { tootls:[{ // 按钮组 type:'group', positon:['left','top'], - list:[{title:'Add limit access',active:true, event:function(ev){ + list:[{title:'Add limit access',active:true, event:function(ev){ site.edit.template_Dir(web.id,true); }}] },{ // 批量操作 @@ -2687,7 +4599,7 @@ var site = { return false; } aceEditor.ACE.setValue(ret.data); - aceEditor.ACE.moveCursorTo(0, 0); + aceEditor.ACE.moveCursorTo(0, 0); aceEditor.path = spath; }) } @@ -2815,571 +4727,6 @@ var site = { bt.saveEditor(config); }); }, - set_ssl: function(web) { - $('#webedit-con').html("
        "); - bt.site.get_site_ssl(web.name, function(rdata) { - var _tabs = [ - // { - // title: lan.site.bt_ssl, on: true, callback: function (robj) { - // bt.pub.get_user_info(function (udata) { - // if (udata.status) { - // bt.site.get_domains(web.id, function (ddata) { - // var domains = []; - // for (var i = 0; i < ddata.length; i++) { - // if (ddata[i].name.indexOf('*') == -1) domains.push({ title: ddata[i].name, value: ddata[i].name }); - // } - // var arrs1 = [ - // { title: lan.site.domain, width: '200px', name: 'domains', type: 'select', items: domains }, - // { - // title: ' ', name: 'btsslApply', text: lan.site.btapply, type: 'button', callback: function (sdata) { - // if (sdata.domains.indexOf('www.') != -1) { - // var rootDomain = sdata.domains.split(/www\./)[1]; - // if (!$.inArray(domains, rootDomain)) { - // layer.msg(lan.site.not_resolve_domain.replace('{1}',sdata.domains).replace("{2}",rootDomain), { icon: 2, time: 5000 }); - // return; - // } - // } - // bt.site.get_dv_ssl(sdata.domains, web.path, function (tdata) { - // bt.msg(tdata); - // if (tdata.status) site.ssl.verify_domain(tdata.data.partnerOrderId, web.name); - // }) - // } - // } - // ] - // for (var i = 0; i < arrs1.length; i++) { - // var _form_data = bt.render_form_line(arrs1[i]); - // robj.append(_form_data.html); - // bt.render_clicks(_form_data.clicks); - // } - // var loading = bt.load() - // bt.site.get_order_list(web.name, function (odata) { - // loading.close(); - // robj.append("
        "); - // bt.render({ - // table: '#bt_order_list', - // columns: [ - // { field: 'commonName', title: lan.site.domain }, - // { - // field: 'endtime', width: '70px', title: lan.site.endtime, templet: function (item) { - // return bt.format_data(item.endtime, 'yyyy/MM/dd'); - // } - // }, - // { field: 'stateName', width: '100px', title: lan.site.status }, - // { - // field: 'opt', align: 'right', width: '100px', title: lan.site.operate, templet: function (item) { - // var opt = ''+lan.site.deploy+'' - // if (item.stateCode == 'WF_DOMAIN_APPROVAL') { - // opt = ''+lan.site.domain_validate+''; - // } - // else { - // if (item.setup) opt = lan.site.deployed+' | '+lan.site.turn_off+'' - // } - // return opt; - // } - // } - // ], - // data: odata.data - // }) - // bt.fixed_table('bt_order_list'); - // var helps = [ - // lan.site.ssl_tips1, - // lan.site.ssl_tips2, - // lan.site.ssl_tips3, - // lan.site.ssl_tips4, - // lan.site.ssl_tips5, - // lan.site.ssl_tips6 - // ] - // robj.append(bt.render_help(helps)); - // }) - // }) - // } - // else { - // robj.append('
        '+lan.site.bt_bind_no+'
        '); - // - // var datas = [ - // { title: lan.site.bt_user, name: 'bt_username', value: rdata.email, width: '260px', placeholder: lan.site.phone_input }, - // { title: lan.site.password, type: 'password', name: 'bt_password', value: rdata.email, width: '260px' }, - // { - // title: ' ', items: [ - // { - // text: lan.site.login, name: 'btn_ssl_login', type: 'button', callback: function (sdata) { - // bt.pub.login_btname(sdata.bt_username, sdata.bt_password, function (ret) { - // if (ret.status) site.reload(7); - // }) - // } - // }, - // { - // text: lan.site.bt_reg, name: 'bt_register', type: 'button', callback: function (sdata) { - // window.open('https://www.bt.cn/register.html') - // } - // } - // ] - // } - // ] - // for (var i = 0; i < datas.length; i++) { - // var _form_data = bt.render_form_line(datas[i]); - // robj.append(_form_data.html); - // bt.render_clicks(_form_data.clicks); - // } - // robj.append(bt.render_help([lan.site.bt_ssl_help_1, lan.site.bt_ssl_help_2, lan.site.bt_ssl_help_3, lan.site.bt_ssl_help_4])); - // } - // }) - // - // } - // }, - { - title: "Let's Encrypt", - callback: function(robj) { - acme.get_account_info(function(let_user) {}); - acme.id = web.id; - if (rdata.status && rdata.type == 1) { - var cert_info = ''; - if (rdata.cert_data['notBefore']) { - cert_info = '
        \ - ' + lan.site.deploy_success_cret + '' + lan.site.try_renew_cret + '\ - \ - ' + lan.site.cert_brand + '' + rdata.cert_data.issuer + '\ - ' + lan.site.auth_domain + ' ' + rdata.cert_data.dns.join('、') + '\ - ' + lan.site.expire_time + ' ' + rdata.cert_data.notAfter + '
        ' - } - robj.append('
        ' + cert_info + '
        ' + lan.site.ssl_key + '' + lan.site.ssl_crt + '
        '); - var datas = [{ - items: [ - { name: 'key', width: '48%', height: '220px', type: 'textarea', value: rdata.key }, - { name: 'csr', width: '48%', height: '220px', type: 'textarea', value: rdata.csr } - ] - }, - { - items: [{ - text: lan.site.ssl_close, - name: 'btn_ssl_close', - hide: !rdata.status, - type: 'button', - callback: function(sdata) { - site.ssl.set_ssl_status('CloseSSLConf', web.name); - } - }, - { - text: lan.site.ssl_renew, - name: 'btn_ssl_renew', - hide: !rdata.status, - type: 'button', - callback: function(sdata) { - site.ssl.renew_ssl(web.name, rdata.auth_type, rdata.index); - } - } - ] - } - ] - for (var i = 0; i < datas.length; i++) { - var _form_data = bt.render_form_line(datas[i]); - robj.append(_form_data.html); - bt.render_clicks(_form_data.clicks); - } - robj.find('textarea').css({'background-color':'#f6f6f6','resize':'none'}).attr('readonly', true); - robj.find('[name=csr]').css('margin-right', '0'); - var helps = [ - lan.site.ssl_tips1, - lan.site.ssl_tips2, - lan.site.ssl_tips3, - lan.site.ssl_tips4, - lan.site.ssl_tips5, - ] - robj.append(bt.render_help([lan.site.ssl_help_2, lan.site.ssl_help_3])); - return; - } - bt.site.get_site_domains(web.id, function(ddata) { - var helps = [ - [ - lan.site.bt_ssl_help_5, - lan.site.bt_ssl_help_8, - lan.site.bt_ssl_help_9, - lan.site.ssl_tips5 - ], - [ - lan.site.dns_check_tips1, - lan.site.dns_check_tips2, - lan.site.dns_check_tips3, - lan.site.dns_check_tips4 - ] - ] - var datas = [{ - title: lan.site.checking_mode, - items: [{ - name: 'check_file', - text: lan.site.file_check, - type: 'radio', - callback: function(obj) { - $('.checks_line').remove() - $(obj).siblings().removeAttr('checked'); - - $('.help-info-text').html($(bt.render_help(helps[0]))); - //var _form_data = bt.render_form_line({ title: ' ', class: 'checks_line label-input-group', items: [{ name: 'force', type: 'checkbox', value: true, text: '提前校验域名(提前发现问题,减少失败率)' }] }); - //$(obj).parents('.line').append(_form_data.html); - - $('#ymlist li input[type="checkbox"]').each(function() { - if ($(this).val().indexOf('*') >= 0) { - $(this).parents('li').hide(); - } - }) - } - }, - { - name: 'check_dns', - text: lan.site.check_dns, - type: 'radio', - callback: function(obj) { - $('.checks_line').remove(); - $(obj).siblings().removeAttr('checked'); - $('.help-info-text').html($(bt.render_help(helps[1]))); - $('#ymlist li').show(); - - var arrs_list = [], - arr_obj = {}; - bt.site.get_dns_api(function(api) { - site.dnsapi = {} - - for (var x = 0; x < api.length; x++) { - site.dnsapi[api[x].name] = {} - site.dnsapi[api[x].name].s_key = "None" - site.dnsapi[api[x].name].s_token = "None" - if (api[x].data) { - site.dnsapi[api[x].name].s_key = api[x].data[0].value - site.dnsapi[api[x].name].s_token = api[x].data[1].value - } - arrs_list.push({ title: api[x].title, value: api[x].name }); - arr_obj[api[x].name] = api[x]; - } - - var data = [{ - title: lan.site.choose_dns, - class: 'checks_line', - items: [{ - name: 'dns_select', - width: 'auto', - type: 'select', - items: arrs_list, - callback: function(obj) { - var _val = obj.val(); - $('.set_dns_config').remove(); - var _val_obj = arr_obj[_val]; - var _form = { - title: '', - area: '530px', - list: [], - btns: [{ title: lan.site.turn_off, name: 'close' }] - }; - - var helps = []; - if (_val_obj.data !== false) { - _form.title = lan.site.set + '【' + _val_obj.title + '】' + lan.site.interface; - if(_val_obj.help == "How to get API Token"){ - _val_obj.help = ''+_val_obj.help+"" - } - helps.push(_val_obj.help); - var is_hide = true; - for (var i = 0; i < _val_obj.data.length; i++) { - _form.list.push({ title: _val_obj.data[i].name, name: _val_obj.data[i].key, value: _val_obj.data[i].value }) - if (!_val_obj.data[i].value) is_hide = false; - } - if(_val_obj.title == 'CloudFlare'){ - _form.list.push({html : '
        API-Limit
        '}); - } - _form.btns.push({ - title: lan.site.save, - css: 'btn-success', - name: 'btn_submit_save', - callback: function(ldata, load) { - bt.site.set_dns_api({ pdata: JSON.stringify(ldata) }, function(ret) { - if (ret.status) { - load.close(); - robj.find('input[type="radio"]:eq(0)').trigger('click') - robj.find('input[type="radio"]:eq(1)').trigger('click') - } - bt.msg(ret); - }) - } - }) - if (is_hide) { - obj.after(''); - $('.set_dns_config').click(function() { - var _bs = bt.render_form(_form); - $('div[data-id="form' + _bs + '"]').append(bt.render_help(helps)); - }) - } else { - var _bs = bt.render_form(_form); - $('div[data-id="form' + _bs + '"]').append(bt.render_help(helps)); - } - } - } - }, ] - }, { - title: ' ', - class: 'checks_line label-input-group', - items: [ - { css: 'label-input-group ptb10', text: 'Automatically combine pan-domain names', name: 'app_root', type: 'checkbox' } - ] - }] - for (var i = 0; i < data.length; i++) { - var _form_data = bt.render_form_line(data[i]); - $(obj).parents('.line').append(_form_data.html) - bt.render_clicks(_form_data.clicks); - } - }) - } - }, - ] - }] - - for (var i = 0; i < datas.length; i++) { - var _form_data = bt.render_form_line(datas[i]); - robj.append(_form_data.html); - bt.render_clicks(_form_data.clicks); - } - var _ul = $('
        '); - for (var i = 0; i < ddata.domains.length; i++) { - if (ddata.domains[i].binding === true) continue - _ul.append('
      • ' + ddata.domains[i].name + '
      • '); - } - var _line = $("
        "); - _line.append('' + lan.site.domain + ''); - _line.append(_ul); - robj.append(_line); - robj.find('input[type="radio"]').parent().addClass('label-input-group ptb10'); - $("#ymlist li input").click(function(e) { - e.stopPropagation(); - var a = true; - $("#ymlist li input").each(function () { - var o = $(this).prop("checked"); - if (!o) { - a = false; - return false; - } - }); - $("#ymlist div input").prop("checked",a); - }) - $("#ymlist li").click(function() { - - var o = $(this).find("input"), - a = true; - if (o.prop("checked")) { - o.prop("checked", false) - } else { - o.prop("checked", true); - } - $("#ymlist li input").each(function () { - var o = $(this).prop("checked"); - if (!o) { - a = false; - return false; - } - }); - $("#ymlist div input").prop("checked",a); - }) - $("#ymlist div").click(function() { - var o = $("#ymlist div input"), p = $("#ymlist input"); - if (o.prop("checked")) { - p.prop("checked", true); - } else { - p.prop("checked", false); - } - }) - var _btn_data = bt.render_form_line({ - title: ' ', - text: lan.site.btapply, - name: 'letsApply', - type: 'button', - callback: function(ldata) { - ldata['domains'] = []; - $('#ymlist li input[type="checkbox"]:checked').each(function() { - ldata['domains'].push($(this).val()) - }) - - var auth_type = 'http' - var auth_to = web.id - var auto_wildcard = '0' - if (ldata.check_dns) { - auth_type = 'dns' - auth_to = 'dns' - auto_wildcard = ldata.app_root ? '1' : '0' - if (ldata.dns_select !== auth_to) { - if (!site.dnsapi[ldata.dns_select].s_key) { - layer.msg("No key information is set for the specified dns interface"); - return; - } - auth_to = ldata.dns_select + "|" + site.dnsapi[ldata.dns_select].s_key + "|" + site.dnsapi[ldata.dns_select].s_token; - } - } - acme.apply_cert(ldata['domains'], auth_type, auth_to, auto_wildcard, function(res) { - site.ssl.ssl_result(res, auth_type, web.name); - }) - - } - }); - robj.append(_btn_data.html); - bt.render_clicks(_btn_data.clicks); - - robj.append(bt.render_help(helps[0])); - robj.find('input[type="radio"]:eq(0)').trigger('click') - }) - } - }, - { - title: lan.site.other_ssl, - callback: function(robj) { - var cert_info = ''; - if (rdata.cert_data['notBefore']) { - cert_info = '
        \ - ' + (rdata.status ? lan.site.deploy_success_tips : lan.site.not_deploy_and_save) + '\ - ' + lan.site.cert_brand + '' + rdata.cert_data.issuer + '\ - ' + lan.site.auth_domain + ' ' + rdata.cert_data.dns.join('、') + '\ - ' + lan.site.expire_time + ' ' + rdata.cert_data.notAfter + '
        ' - } - robj.append('
        ' + cert_info + '
        ' + lan.site.ssl_key + '' + lan.site.ssl_crt + '
        '); - var datas = [{ - items: [ - { name: 'key', width: '48%', height: '220px', type: 'textarea', value: rdata.key }, - { name: 'csr', width: '48%', height: '220px', type: 'textarea', value: rdata.csr } - ] - }, - { - items: [{ - text: lan.site.save, - name: 'btn_ssl_save', - type: 'button', - callback: function(sdata) { - bt.site.set_ssl(web.name, sdata, function(ret) { - if (ret.status) site.reload(7); - bt.msg(ret); - }) - } - }, - { - text: lan.site.ssl_close, - name: 'btn_ssl_close', - hide: !rdata.status, - type: 'button', - callback: function(sdata) { - site.ssl.set_ssl_status('CloseSSLConf', web.name); - } - } - ] - } - ] - for (var i = 0; i < datas.length; i++) { - var _form_data = bt.render_form_line(datas[i]); - robj.append(_form_data.html); - bt.render_clicks(_form_data.clicks); - } - var helps = [ - lan.site.bt_ssl_help_10, - lan.public_backup.cret_err, - lan.public_backup.pem_format, - lan.site.ssl_tips5, - ] - robj.append(bt.render_help(helps)); - robj.find(".help-info-text").css('margin-top','0'); - robj.find('textarea').css('resize','none'); - robj.find('[name=csr]').css('margin-right', '0'); - } - }, - { - title: lan.site.turn_off, - callback: function(robj) { - if (rdata.type == -1) { - robj.html("
        " + lan.site.ssl_help_1 + "
        "); - return; - }; - var txt = ''; - switch (rdata.type) { - case 1: - txt = "Let's Encrypt"; - break; - case 0: - txt = lan.site.other_ssl; - break; - case 2: - txt = lan.site.bt_ssl; - break; - } - $(".tab-con").html("
        " + lan.get('ssl_enable', [txt]) + "
        "); - - } - }, - { - title: lan.site.ssl_dir, - callback: function(robj) { - robj.html("
        "); - bt.site.get_cer_list(function(rdata) { - bt.render({ - table: '#cer_list_table', - columns: [{ - field: 'subject', - title: lan.site.domain, - templet: function(item) { - return item.dns.join('
        ') - } - }, - { field: 'notAfter', width: '100px', title: lan.site.endtime }, - { field: 'issuer', width: '150px', title: lan.site.brand }, - { - field: 'opt', - width: '100px', - align: 'right', - title: lan.site.operate, - templet: function(item) { - var opt = '' + lan.site.deploy + ' | '; - opt += '' + lan.site.del + '' - return opt; - } - } - ], - data: rdata - }) - }) - } - } - ] - bt.render_tab('ssl_tabs', _tabs); - $('#ssl_tabs').append('
        ' + lan.site.force_https + '
        '); - $("#toHttps").attr('checked', rdata.httpTohttps); - $('#toHttps').click(function(sdata) { - var isHttps = $("#toHttps").attr('checked'); - if (isHttps) { - layer.confirm('After closing HTTPS, you need to clear your browser cache to see the effect. Continue?', { icon: 3, title: "Turn off forced HTTPS\"" }, function() { - bt.site.close_http_to_https(web.name, function(rdata) { - if (rdata.status) { - setTimeout(function() { - site.reload(7); - }, 3000); - } - }) - }); - } else { - bt.site.set_http_to_https(web.name, function(rdata) { - if (!rdata.status) { - setTimeout(function() { - site.reload(7); - }, 3000); - } - - }) - } - }) - switch (rdata.type) { - case 1: - $('#ssl_tabs span:eq(0)').trigger('click'); - break; - case 0: - $('#ssl_tabs span:eq(0)').trigger('click'); - break; - default: - $('#ssl_tabs span:eq(0)').trigger('click'); - break; - } - - }) - }, set_php_version: function(web) { bt.site.get_site_phpversion(web.name, function(sdata) { if (sdata.status === false) { @@ -3978,7 +5325,7 @@ var site = { tootls:[{ //按钮组 type:'group', positon:['left','top'], - list:[{title:'Add redirection',active:true, event:function(ev){ + list:[{title:'Add redirection',active:true, event:function(ev){ site.edit.templet_301(web.name,web.id,true); }}] },{ //批量操作 @@ -4170,7 +5517,7 @@ var site = { var limit_len = bt.get_cookie('serverType') == 'nginx'?'proxy_list_limit_4':'proxy_list_limit_3'; $('#webedit-con').html('
        '); String.prototype.myReplace = function (f, e) {//吧f替换成e - var reg = new RegExp(f, "g"); //创建正则RegExp对象 + var reg = new RegExp(f, "g"); //创建正则RegExp对象 return this.replace(reg, e); } bt_tools.table({ @@ -4278,7 +5625,7 @@ var site = { tootls:[{ //按钮组 type:'group', positon:['left','top'], - list:[{title:'Add reverse proxy',active:true, event:function(ev){ + list:[{title:'Add reverse proxy',active:true, event:function(ev){ site.edit.templet_proxy(web.name, true) }}] },{ //批量操作 @@ -4497,7 +5844,7 @@ var site = { { title: lan.site.site_menu_4, callback: site.edit.get_rewrite_list }, { title: lan.site.site_menu_5, callback: site.edit.set_default_index }, { title: lan.site.site_menu_6, callback: site.edit.set_config }, - { title: lan.site.site_menu_7, callback: site.edit.set_ssl }, + { title: lan.site.site_menu_7, callback: site.set_ssl }, { title: lan.site.php_ver, callback: site.edit.set_php_version }, { title: 'Composer', callback: site.edit.set_composer }, // { title: lan.site.site_menu_9, callback: site.edit.set_tomact }, @@ -4522,20 +5869,594 @@ var site = { }) site.reload(0); }, 100) + }, + + set_ssl: function(web) { + // if(typeof web['ele'] === 'undefined') web['ele'] = $('.webedit-con') + $('#webedit-con').html("
        "); + bt.site.get_site_ssl(web.name, function(rdata) { + var _tabs = [ + // { + // title: lan.site.bt_ssl, on: true, callback: function (robj) { + // bt.pub.get_user_info(function (udata) { + // if (udata.status) { + // bt.site.get_domains(web.id, function (ddata) { + // var domains = []; + // for (var i = 0; i < ddata.length; i++) { + // if (ddata[i].name.indexOf('*') == -1) domains.push({ title: ddata[i].name, value: ddata[i].name }); + // } + // var arrs1 = [ + // { title: lan.site.domain, width: '200px', name: 'domains', type: 'select', items: domains }, + // { + // title: ' ', name: 'btsslApply', text: lan.site.btapply, type: 'button', callback: function (sdata) { + // if (sdata.domains.indexOf('www.') != -1) { + // var rootDomain = sdata.domains.split(/www\./)[1]; + // if (!$.inArray(domains, rootDomain)) { + // layer.msg(lan.site.not_resolve_domain.replace('{1}',sdata.domains).replace("{2}",rootDomain), { icon: 2, time: 5000 }); + // return; + // } + // } + // bt.site.get_dv_ssl(sdata.domains, web.path, function (tdata) { + // bt.msg(tdata); + // if (tdata.status) site.ssl.verify_domain(tdata.data.partnerOrderId, web.name); + // }) + // } + // } + // ] + // for (var i = 0; i < arrs1.length; i++) { + // var _form_data = bt.render_form_line(arrs1[i]); + // robj.append(_form_data.html); + // bt.render_clicks(_form_data.clicks); + // } + // var loading = bt.load() + // bt.site.get_order_list(web.name, function (odata) { + // loading.close(); + // robj.append("
        "); + // bt.render({ + // table: '#bt_order_list', + // columns: [ + // { field: 'commonName', title: lan.site.domain }, + // { + // field: 'endtime', width: '70px', title: lan.site.endtime, templet: function (item) { + // return bt.format_data(item.endtime, 'yyyy/MM/dd'); + // } + // }, + // { field: 'stateName', width: '100px', title: lan.site.status }, + // { + // field: 'opt', align: 'right', width: '100px', title: lan.site.operate, templet: function (item) { + // var opt = ''+lan.site.deploy+'' + // if (item.stateCode == 'WF_DOMAIN_APPROVAL') { + // opt = ''+lan.site.domain_validate+''; + // } + // else { + // if (item.setup) opt = lan.site.deployed+' | '+lan.site.turn_off+'' + // } + // return opt; + // } + // } + // ], + // data: odata.data + // }) + // bt.fixed_table('bt_order_list'); + // var helps = [ + // lan.site.ssl_tips1, + // lan.site.ssl_tips2, + // lan.site.ssl_tips3, + // lan.site.ssl_tips4, + // lan.site.ssl_tips5, + // lan.site.ssl_tips6 + // ] + // robj.append(bt.render_help(helps)); + // }) + // }) + // } + // else { + // robj.append('
        '+lan.site.bt_bind_no+'
        '); + // + // var datas = [ + // { title: lan.site.bt_user, name: 'bt_username', value: rdata.email, width: '260px', placeholder: lan.site.phone_input }, + // { title: lan.site.password, type: 'password', name: 'bt_password', value: rdata.email, width: '260px' }, + // { + // title: ' ', items: [ + // { + // text: lan.site.login, name: 'btn_ssl_login', type: 'button', callback: function (sdata) { + // bt.pub.login_btname(sdata.bt_username, sdata.bt_password, function (ret) { + // if (ret.status) site.reload(7); + // }) + // } + // }, + // { + // text: lan.site.bt_reg, name: 'bt_register', type: 'button', callback: function (sdata) { + // window.open('https://www.bt.cn/register.html') + // } + // } + // ] + // } + // ] + // for (var i = 0; i < datas.length; i++) { + // var _form_data = bt.render_form_line(datas[i]); + // robj.append(_form_data.html); + // bt.render_clicks(_form_data.clicks); + // } + // robj.append(bt.render_help([lan.site.bt_ssl_help_1, lan.site.bt_ssl_help_2, lan.site.bt_ssl_help_3, lan.site.bt_ssl_help_4])); + // } + // }) + // + // } + // }, + { + title: "Let's Encrypt", + callback: function(robj) { + robj = $('#webedit-con .tab-con') + console.log(robj,'obj'); + acme.get_account_info(function(let_user) {}); + acme.id = web.id; + if (rdata.status && rdata.type == 1) { + var cert_info = ''; + if (rdata.cert_data['notBefore']) { + cert_info = '
        \ + ' + lan.site.deploy_success_cret + '' + lan.site.try_renew_cret + '\ + \ + ' + lan.site.cert_brand + '' + rdata.cert_data.issuer + '\ + ' + lan.site.auth_domain + ' ' + rdata.cert_data.dns.join('、') + '\ + ' + lan.site.expire_time + ' ' + rdata.cert_data.notAfter + '
        ' + } + robj.append('
        ' + cert_info + '
        ' + lan.site.ssl_key + '' + lan.site.ssl_crt + '
        '); + var datas = [{ + items: [ + { name: 'key', width: '48%', height: '220px', type: 'textarea', value: rdata.key }, + { name: 'csr', width: '48%', height: '220px', type: 'textarea', value: rdata.csr } + ] + }, + { + items: [{ + text: lan.site.ssl_close, + name: 'btn_ssl_close', + hide: !rdata.status, + type: 'button', + callback: function(sdata) { + site.ssl.set_ssl_status('CloseSSLConf', web.name); + } + }, + { + text: lan.site.ssl_renew, + name: 'btn_ssl_renew', + hide: !rdata.status, + type: 'button', + callback: function(sdata) { + site.ssl.renew_ssl(web.name, rdata.auth_type, rdata.index); + } + } + ] + } + ] + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + robj.find('textarea').css({'background-color':'#f6f6f6','resize':'none'}).attr('readonly', true); + robj.find('[name=csr]').css('margin-right', '0'); + var helps = [ + lan.site.ssl_tips1, + lan.site.ssl_tips2, + lan.site.ssl_tips3, + lan.site.ssl_tips4, + lan.site.ssl_tips5, + ] + robj.append(bt.render_help([lan.site.ssl_help_2, lan.site.ssl_help_3])); + return; + } + bt.site.get_site_domains(web.id, function(ddata) { + var helps = [ + [ + lan.site.bt_ssl_help_5, + lan.site.bt_ssl_help_8, + lan.site.bt_ssl_help_9, + lan.site.ssl_tips5 + ], + [ + lan.site.dns_check_tips1, + lan.site.dns_check_tips2, + lan.site.dns_check_tips3, + lan.site.dns_check_tips4 + ] + ] + var datas = [{ + title: lan.site.checking_mode, + items: [{ + name: 'check_file', + text: lan.site.file_check, + type: 'radio', + callback: function(obj) { + $('.checks_line').remove() + $(obj).siblings().removeAttr('checked'); + + $('.help-info-text').html($(bt.render_help(helps[0]))); + //var _form_data = bt.render_form_line({ title: ' ', class: 'checks_line label-input-group', items: [{ name: 'force', type: 'checkbox', value: true, text: '提前校验域名(提前发现问题,减少失败率)' }] }); + //$(obj).parents('.line').append(_form_data.html); + + $('#ymlist li input[type="checkbox"]').each(function() { + if ($(this).val().indexOf('*') >= 0) { + $(this).parents('li').hide(); + } + }) + } + }, + { + name: 'check_dns', + text: lan.site.check_dns, + type: 'radio', + callback: function(obj) { + $('.checks_line').remove(); + $(obj).siblings().removeAttr('checked'); + $('.help-info-text').html($(bt.render_help(helps[1]))); + $('#ymlist li').show(); + + var arrs_list = [], + arr_obj = {}; + bt.site.get_dns_api(function(api) { + site.dnsapi = {} + + for (var x = 0; x < api.length; x++) { + site.dnsapi[api[x].name] = {} + site.dnsapi[api[x].name].s_key = "None" + site.dnsapi[api[x].name].s_token = "None" + if (api[x].data) { + site.dnsapi[api[x].name].s_key = api[x].data[0].value + site.dnsapi[api[x].name].s_token = api[x].data[1].value + } + arrs_list.push({ title: api[x].title, value: api[x].name }); + arr_obj[api[x].name] = api[x]; + } + + var data = [{ + title: lan.site.choose_dns, + class: 'checks_line', + items: [{ + name: 'dns_select', + width: 'auto', + type: 'select', + items: arrs_list, + callback: function(obj) { + var _val = obj.val(); + $('.set_dns_config').remove(); + var _val_obj = arr_obj[_val]; + var _form = { + title: '', + area: '530px', + list: [], + btns: [{ title: lan.site.turn_off, name: 'close' }] + }; + + var helps = []; + if (_val_obj.data !== false) { + _form.title = lan.site.set + '【' + _val_obj.title + '】' + lan.site.interface; + if(_val_obj.help == "How to get API Token"){ + _val_obj.help = ''+_val_obj.help+"" + } + helps.push(_val_obj.help); + var is_hide = true; + for (var i = 0; i < _val_obj.data.length; i++) { + _form.list.push({ title: _val_obj.data[i].name, name: _val_obj.data[i].key, value: _val_obj.data[i].value }) + if (!_val_obj.data[i].value) is_hide = false; + } + if(_val_obj.title == 'CloudFlare'){ + _form.list.push({html : '
        API-Limit
        '}); + } + _form.btns.push({ + title: lan.site.save, + css: 'btn-success', + name: 'btn_submit_save', + callback: function(ldata, load) { + bt.site.set_dns_api({ pdata: JSON.stringify(ldata) }, function(ret) { + if (ret.status) { + load.close(); + robj.find('input[type="radio"]:eq(0)').trigger('click') + robj.find('input[type="radio"]:eq(1)').trigger('click') + } + bt.msg(ret); + }) + } + }) + if (is_hide) { + obj.after(''); + $('.set_dns_config').click(function() { + var _bs = bt.render_form(_form); + $('div[data-id="form' + _bs + '"]').append(bt.render_help(helps)); + }) + } else { + var _bs = bt.render_form(_form); + $('div[data-id="form' + _bs + '"]').append(bt.render_help(helps)); + } + } + } + }, ] + }, { + title: ' ', + class: 'checks_line label-input-group', + items: [ + { css: 'label-input-group ptb10', text: 'Automatically combine pan-domain names', name: 'app_root', type: 'checkbox' } + ] + }] + for (var i = 0; i < data.length; i++) { + var _form_data = bt.render_form_line(data[i]); + $(obj).parents('.line').append(_form_data.html) + bt.render_clicks(_form_data.clicks); + } + }) + } + }, + ] + }] + + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + var _ul = $('
        '); + for (var i = 0; i < ddata.domains.length; i++) { + if (ddata.domains[i].binding === true) continue + _ul.append('
      • ' + ddata.domains[i].name + '
      • '); + } + var _line = $("
        "); + _line.append('' + lan.site.domain + ''); + _line.append(_ul); + robj.append(_line); + robj.find('input[type="radio"]').parent().addClass('label-input-group ptb10'); + $("#ymlist li input").click(function(e) { + e.stopPropagation(); + var a = true; + $("#ymlist li input").each(function () { + var o = $(this).prop("checked"); + if (!o) { + a = false; + return false; + } + }); + $("#ymlist div input").prop("checked",a); + }) + $("#ymlist li").click(function() { + + var o = $(this).find("input"), + a = true; + if (o.prop("checked")) { + o.prop("checked", false) + } else { + o.prop("checked", true); + } + $("#ymlist li input").each(function () { + var o = $(this).prop("checked"); + if (!o) { + a = false; + return false; + } + }); + $("#ymlist div input").prop("checked",a); + }) + $("#ymlist div").click(function() { + var o = $("#ymlist div input"), p = $("#ymlist input"); + if (o.prop("checked")) { + p.prop("checked", true); + } else { + p.prop("checked", false); + } + }) + var _btn_data = bt.render_form_line({ + title: ' ', + text: lan.site.btapply, + name: 'letsApply', + type: 'button', + callback: function(ldata) { + ldata['domains'] = []; + $('#ymlist li input[type="checkbox"]:checked').each(function() { + ldata['domains'].push($(this).val()) + }) + + var auth_type = 'http' + var auth_to = web.id + var auto_wildcard = '0' + if (ldata.check_dns) { + auth_type = 'dns' + auth_to = 'dns' + auto_wildcard = ldata.app_root ? '1' : '0' + if (ldata.dns_select !== auth_to) { + if (!site.dnsapi[ldata.dns_select].s_key) { + layer.msg("No key information is set for the specified dns interface"); + return; + } + auth_to = ldata.dns_select + "|" + site.dnsapi[ldata.dns_select].s_key + "|" + site.dnsapi[ldata.dns_select].s_token; + } + } + acme.apply_cert(ldata['domains'], auth_type, auth_to, auto_wildcard, function(res) { + site.ssl.ssl_result(res, auth_type, web.name); + }) + + } + }); + robj.append(_btn_data.html); + bt.render_clicks(_btn_data.clicks); + + robj.append(bt.render_help(helps[0])); + robj.find('input[type="radio"]:eq(0)').trigger('click') + }) + } + }, + { + title: lan.site.other_ssl, + callback: function(robj) { + robj = $('#webedit-con .tab-con') + var cert_info = ''; + if (rdata.cert_data['notBefore']) { + cert_info = '
        \ + ' + (rdata.status ? lan.site.deploy_success_tips : lan.site.not_deploy_and_save) + '\ + ' + lan.site.cert_brand + '' + rdata.cert_data.issuer + '\ + ' + lan.site.auth_domain + ' ' + rdata.cert_data.dns.join('、') + '\ + ' + lan.site.expire_time + ' ' + rdata.cert_data.notAfter + '
        ' + } + robj.append('
        ' + cert_info + '
        ' + lan.site.ssl_key + '' + lan.site.ssl_crt + '
        '); + var datas = [{ + items: [ + { name: 'key', width: '48%', height: '220px', type: 'textarea', value: rdata.key }, + { name: 'csr', width: '48%', height: '220px', type: 'textarea', value: rdata.csr } + ] + }, + { + items: [{ + text: lan.site.save, + name: 'btn_ssl_save', + type: 'button', + callback: function(sdata) { + bt.site.set_ssl(web.name, sdata, function(ret) { + if (ret.status) site.reload(7); + bt.msg(ret); + }) + } + }, + { + text: lan.site.ssl_close, + name: 'btn_ssl_close', + hide: !rdata.status, + type: 'button', + callback: function(sdata) { + site.ssl.set_ssl_status('CloseSSLConf', web.name); + } + } + ] + } + ] + for (var i = 0; i < datas.length; i++) { + var _form_data = bt.render_form_line(datas[i]); + robj.append(_form_data.html); + bt.render_clicks(_form_data.clicks); + } + var helps = [ + lan.site.bt_ssl_help_10, + lan.public_backup.cret_err, + lan.public_backup.pem_format, + lan.site.ssl_tips5, + ] + robj.append(bt.render_help(helps)); + robj.find(".help-info-text").css('margin-top','0'); + robj.find('textarea').css('resize','none'); + robj.find('[name=csr]').css('margin-right', '0'); + } + }, + { + title: lan.site.turn_off, + callback: function(robj) { + robj = $('#webedit-con .tab-con') + if (rdata.type == -1) { + robj.html("
        " + lan.site.ssl_help_1 + "
        "); + return; + }; + var txt = ''; + switch (rdata.type) { + case 1: + txt = "Let's Encrypt"; + break; + case 0: + txt = lan.site.other_ssl; + break; + case 2: + txt = lan.site.bt_ssl; + break; + } + $("#webedit-con .tab-con").html("
        " + lan.get('ssl_enable', [txt]) + "
        "); + + } + }, + { + title: lan.site.ssl_dir, + callback: function(robj) { + robj = $('#webedit-con .tab-con') + robj.html("
        "); + bt.site.get_cer_list(function(rdata) { + bt.render({ + table: '#cer_list_table', + columns: [{ + field: 'subject', + title: lan.site.domain, + templet: function(item) { + return item.dns.join('
        ') + } + }, + { field: 'notAfter', width: '100px', title: lan.site.endtime }, + { field: 'issuer', width: '150px', title: lan.site.brand }, + { + field: 'opt', + width: '100px', + align: 'right', + title: lan.site.operate, + templet: function(item) { + var opt = '' + lan.site.deploy + ' | '; + opt += '' + lan.site.del + '' + return opt; + } + } + ], + data: rdata + }) + }) + } + } + ] + bt.render_tab('ssl_tabs', _tabs); + $('#ssl_tabs').append('
        ' + lan.site.force_https + '
        '); + $("#toHttps").attr('checked', rdata.httpTohttps); + $('#toHttps').click(function(sdata) { + var isHttps = $("#toHttps").attr('checked'); + if (isHttps) { + layer.confirm('After closing HTTPS, you need to clear your browser cache to see the effect. Continue?', { icon: 3, title: "Turn off forced HTTPS\"" }, function() { + bt.site.close_http_to_https(web.name, function(rdata) { + if (rdata.status) { + setTimeout(function() { + site.reload(7); + }, 3000); + } + }) + }); + } else { + bt.site.set_http_to_https(web.name, function(rdata) { + if (!rdata.status) { + setTimeout(function() { + site.reload(7); + }, 3000); + } + + }) + } + }) + // switch (rdata.type) { + // case 1: + // $('#ssl_tabs span:eq(0)').trigger('click'); + // break; + // case 0: + // $('#ssl_tabs span:eq(0)').trigger('click'); + // break; + // default: + // $('#ssl_tabs span:eq(0)').trigger('click'); + // break; + // } + $('#ssl_tabs span:eq(0)').trigger('click'); + }) } } -site.get_types(); -$.prototype.serializeObject = function() { - var a, o, h, i, e; - a = this.serializeArray(); - o = {}; - h = o.hasOwnProperty; - for (i = 0; i < a.length; i++) { - e = a[i]; - if (!h.call(o, e.name)) { - o[e.name] = e.value; - } - } - return o; -}; \ No newline at end of file +$('.site_table_view .tab-nav span').eq(bt.get_cookie('site_tab_status') == null?0:(bt.get_cookie('site_tab_status') == 'php'?0:1)).trigger('click') +// site.get_types(); + +// $.prototype.serializeObject = function() { +// var a, o, h, i, e; +// a = this.serializeArray(); +// o = {}; +// h = o.hasOwnProperty; +// for (i = 0; i < a.length; i++) { +// e = a[i]; +// if (!h.call(o, e.name)) { +// o[e.name] = e.value; +// } +// } +// return o; +// }; \ No newline at end of file diff --git a/BTPanel/static/js/tools.js b/BTPanel/static/js/tools.js index 3f79e16c..34443ba6 100644 --- a/BTPanel/static/js/tools.js +++ b/BTPanel/static/js/tools.js @@ -6,151 +6,152 @@ var bt_tools = { * @param {object} config 配置对象 参考说明 * @return 当前实例对象 */ - table:function(config){ - function ReaderTable(config){ + table: function (config) { + var that = this; + function ReaderTable(config) { this.config = config; this.$load(); } ReaderTable.prototype = { - style_list:[], // 样式列表 - event_list:{}, // 事件列表,已绑定事件 - checkbox_list:[], // 元素选中列表 - batch_active:{}, - event_rows_model:{},// 事件行内元素模型,行内元素点击都会将数据临时存放 - data:[], - page:'', - column:[], - batch_thread:[], - random:bt.get_random(5), - init:false, // 是否初始化渲染 - checked:false, // 是否激活,用来判断是否失去焦点 + style_list: [], // 样式列表 + event_list: {}, // 事件列表,已绑定事件 + checkbox_list: [], // 元素选中列表 + batch_active: {}, + event_rows_model: {}, // 事件行内元素模型,行内元素点击都会将数据临时存放 + data: [], + page: '', + column: [], + batch_thread: [], + random: bt.get_random(5), + init: false, // 是否初始化渲染 + checked: false, // 是否激活,用来判断是否失去焦点 /** * @description 加载数据 * @return void */ - $load:function(){ - var _that = this,$checked = $(''); - if(this.config.init) this.config.init(this); - $(this.config.el).addClass('bt_table').append($checked); - $checked.focus(function(){ - _that.checked = true; - return false; - }).blur(function(ev){ - _that.checked = false; - if(_that.checked_blur) _that.checked_blur(ev); + $load: function () { + var _that = this; + if (this.config.init) this.config.init(this); + $(this.config.el).addClass('bt_table'); + if (this.config.minWidth) this.style_list.push({ + className: this.config.el + ' table', + css: ('min-width:' + this.config.minWidth) }); - if(this.config.minWidth) this.style_list.push({className:this.config.el + ' table',css:('min-width:'+this.config.minWidth)}); - if(this.config.tootls){ + if (this.config.tootls) { this.$reader_tootls(this.config.tootls); - }else{ - console.log(this.config.height) - if ($(_that.config.el + '.divtable').length === 0) $(_that.config.el).append('
        '); + } else { + if ($(_that.config.el + '.divtable').length === 0) $(_that.config.el).append('
        '); } this.$reader_content(); - if(_that.config.url !== undefined){ - this.$refresh_table_list(); - }else if(this.config.data !== undefined){ + if (_that.config.url !== undefined){ + this.$refresh_table_list(_that.config.load || false); + } else if (this.config.data !== undefined) { this.$reader_content(this.config.data); - }else { - alert(lan['public'].miss_data_or_url); + } else { + alert('缺少data或url参数'); } - if(this.config.methods){ //挂载实例方法 - $.extend(this,this.config.methods); + if (this.config.methods) { //挂载实例方法 + $.extend(this, this.config.methods); } + if(this.config.height) bt_tools.$fixed_table_thead(this.config.el +' .divtable') }, /** * @description 刷新表格数据 + * @param {boolean} load + * @param {function} callback 回调函数 * @return void - */ - $refresh_table_list:function(load){ + */ + $refresh_table_list: function (load,callback) { var _that = this,loadT; - if(load) loadT = bt.load(lan.database.get_data); - this.$http(function(data,page){ - if(load) loadT.close(); - _that.$reader_content(data,page); + if(load) loadT = bt_tools.load(lan.database.get_data); + this.$http(function(data) { + if (loadT) loadT.close(); + if (callback) callback(data); + _that.$reader_content(data.data,typeof data.total != "undefined"?parseInt(data.total):data.page); }); }, - /** - * @description 激活焦点 - * @param {Function} callback 回调函数 - * @returns void - */ - $active_focus:function(callback){ - var _that = this; - $('#checked_'+ _that.random).focus(); - this.checked_blur = function(ev){ - setTimeout(function(){ - if(callback) callback(ev); - delete _that.checked_blur; - },200); - } - }, /** * @description 渲染内容 * @param {object} data 渲染的数据 - * @param {number} page 数据分页 + * @param {number} page 数据分页/总数 * @return void */ - $reader_content:function(data,page){ + $reader_content: function (data,page) { var _that = this,thead = '',tbody = '',i = 0,column = this.config.column,event_list = {},checkbox = $(_that.config.el + ' .checkbox_' + _that.random); data = data || []; this.data = data; - if(checkbox.length){ + if (checkbox.length) { checkbox.removeClass('active selected'); _that.checkbox_list = []; _that.$set_batch_view(); } - do{ - var rows = data[i],completion = 0; - if(data.length > 0) tbody += ''; - for(var j=0;j < column.length;j++){ + do { + var rows = data[i], + completion = 0; + if (data.length > 0) tbody += ''; + for (var j = 0; j < column.length; j++) { var item = column[j]; - if($.isEmptyObject(item)){ - completion ++; + if ($.isEmptyObject(item)) { + completion++; continue; } - if(i === 0 && !this.init){ - if(!this.init) this.style_list.push(this.$dynamic_merge_style(item,j - completion)); - var sortName = 'sort_'+ this.random +'',checkboxName = 'checkbox_'+ this.random,sortValue = item.sortValue || 'desc'; - thead += ''+ (item.type =="checkbox"?'':''+ item.title +'') + (item.sort?'':'') +''; - if(i === 0){ - if(!event_list[sortName] && item.sort) event_list[sortName] = {event:this.config.sortEvent,eventType:'click',type:'sort'}; - if(!event_list[checkboxName]) event_list[checkboxName] = {event:item.checked,eventType:'click',type:'checkbox'}; + if (i === 0 && !this.init) { + if (!this.init) this.style_list.push(this.$dynamic_merge_style(item, j - completion)); + var sortName = 'sort_' + this.random + '', + checkboxName = 'checkbox_' + this.random, + sortValue = item.sortValue || 'desc'; + thead += '' + (item.type == "checkbox" ? '' : '' + item.title + '') + (item.sort ? '' : '') + ''; + if (i === 0) { + if (!event_list[sortName] && item.sort) event_list[sortName] = { + event: this.config.sortEvent, + eventType: 'click', + type: 'sort' + }; + if (!event_list[checkboxName]) event_list[checkboxName] = { + event: item.checked, + eventType: 'click', + type: 'checkbox' + }; } } - if(rows !== undefined){ - var template = '',className = 'event-'+ item.fid +'-'+ this.random; + if (rows !== undefined){ + var template = '',className = 'event_' + item.fid + '_' + this.random; if(item.template){ - template = _that.$custom_template_render(item,rows,j); - }else{ - template = this.$reader_column_type(item,rows); - event_list = $.extend(event_list,template[1]); + template = _that.$custom_template_render(item, rows, j); + } + if(typeof template === "undefined" || typeof item.template === "undefined"){ + template = this.$reader_column_type(item, rows); + event_list = $.extend(event_list, template[1]); template = template[0]; } - tbody += ''+ template +''; - if(i === 0){ - if(!event_list[className] && item.event) event_list[className] = {event:item.event,eventType:'click',type:'rows'}; + tbody += '' + template + ''; + if (i === 0) { + if (!event_list[className] && item.event) event_list[className] = { + event: item.event, + eventType: 'click', + type: 'rows' + }; } } } - if(data.length > 0) tbody += '' - if(data.length == 0) tbody += ''+ (this.config['default'] || lan['public'].empty) +''; + if (data.length > 0) tbody += '' + if (data.length == 0) tbody += '' + (this.config['default'] || lan['public'].empty) + ''; i++; - } while (i < data.length); - if(!this.init) this.$style_bind(this.style_list); + } while (i < data.length); + if (!this.init) this.$style_bind(this.style_list); this.$event_bind(event_list); - if(!this.init){ - $(this.config.el + ' .divtable').append(''+ thead +''+ tbody +'
        '); - }else{ + if (!this.init) { + $(this.config.el + ' .divtable').append('' + thead + '' + tbody + '
        '); + } else { $(this.config.el + ' .divtable tbody').html(tbody); - if(this.config.page && page){ + if (this.config.page){ $(this.config.el + ' .page').replaceWith(this.$reader_page(this.config.page,page)); } } this.init = true; - if(this.config.success) this.config.success(this); + if (this.config.success) this.config.success(this); }, /** * @description 自定模板渲染 @@ -159,51 +160,61 @@ var bt_tools = { * @param {number} j 当前模板index * @return void */ - $custom_template_render:function(item,rows,j){ - var className= 'event-'+ item.fid +'-'+ this.random,_template = item.template(rows,j),$template = $(_template); - if($template.length>0){ + $custom_template_render: function (item, rows, j) { + var className = 'event_' + item.fid + '_' + this.random, + _template = item.template(rows, j), + $template = $(_template); + if ($template.length > 0) { template = $template.addClass(className)[0].outerHTML; - }else{ - if(item.type === 'text'){ - template = ''+ _template +''; - }else{ - template = ''+ _template +''; + } else { + if (item.type === 'text') { + template = '' + _template + ''; + } else { + template = '' + _template + ''; } } return template; }, + /** * @description 替换table数据 * @param {string} newValue 内容数据 * @return void */ - $modify_row_data:function(newValue){ - this.event_rows_model.rows = $.extend(this.event_rows_model.rows,newValue); + $modify_row_data: function (newValue) { + this.event_rows_model.rows = $.extend(this.event_rows_model.rows, newValue); var row_model = this.event_rows_model,template = null; - if(typeof row_model.model.template != 'undefined'){ - template = $(this.$custom_template_render(row_model.model,row_model.rows,row_model.index)); - }else{ - template = $(this.$reader_column_type(row_model.model,row_model.rows)[0]); + if (typeof row_model.model.template != 'undefined') { + template = $(this.$custom_template_render(row_model.model, row_model.rows, row_model.index)); + if(!template.length) template = $(this.$reader_column_type(row_model.model, row_model.rows)[0]); + } else { + template = $(this.$reader_column_type(row_model.model, row_model.rows)[0]); + } + if(row_model.model.type == 'group'){ + $(row_model.el).parent().empty().append(template); + }else{ + row_model.el.replaceWith(template); } - row_model.el.replaceWith(template); row_model.el = template; }, + /** * @description 批量执行程序 * @param {object} config 配置文件 * @return void */ - $batch_success_table:function(config){ - var _that = this; + $batch_success_table: function (config) { + console.log(1); + var _that = this,length = $(config.html).length; bt.open({ - type:1, - title:config.title, - area:config.area || ['380px','350px'], - shadeClose:false, - closeBtn:2, - content:config.content || '
        '+ config.title +' '+lan['public'].success+'
        '+ config.html +'
        '+ config.th +''+lan['public'].result+'
        ', - success:function(){ - _that.$fixed_table_thead('.fiexd_thead'); + type: 1, + title: config.title, + area: config.area || ['400px', '350px'], + shadeClose: false, + closeBtn: 2, + content: config.content || '
        ' + config.title + ' '+lan['public'].success+'
        ' + config.html + '
        ' + config.th + ''+lan['public'].result+'
        ', + success: function () { + if(length > 4) _that.$fixed_table_thead('.fiexd_thead'); } }); }, @@ -211,29 +222,17 @@ var bt_tools = { /** * @description 删除行内数据 */ - $delete_table_row:function(index){ - this.data.splice(index,1) + $delete_table_row: function (index) { + this.data.splice(index, 1) this.$reader_content(this.data); }, - /** - * @description 固定表头 - * @param {string} el DOM选择器 - * @return void - */ - $fixed_table_thead:function(el){ - $(el).scroll(function(){ - var scrollTop = this.scrollTop; - this.querySelector('thead').style.transform = 'translateY(' + scrollTop + 'px)'; - }); - }, + /** * @description 设置批量操作显示 - * @returns void 无 + * @return void 无 */ - $set_batch_view:function(){ - var bt_select_val = $(this.config.el + ' .bt_select_value'), - bt_select_btn = $(this.config.el + ' .bt_table_select_group').next(); + $set_batch_view: function () { if (typeof this.config.batch != "undefined") { //判断是否存在批量操作 var bt_select_btn = $(this.config.el + ' .set_batch_option'); if (typeof this.config.batch.config != "undefined") { // 判断批量操作是多个还是单个 @@ -243,13 +242,13 @@ var bt_tools = { bt_select_btn.addClass('bt-disabled btn-default').removeClass('btn-success').text(lan['public'].please_choose + this.batch_active.title); } } else { - var bt_select_val = $(this.config.el + ' .bt_select_value'); + var bt_select_val = $(this.config.el + ' .bt_table_select_group .bt_select_value'); if (this.checkbox_list.length > 0) { bt_select_btn.removeClass('bt-disabled btn-default').addClass('btn-success').prev().removeClass('bt-disabled'); bt_select_val.find('em').html('(' + lan['public'].selected + this.checkbox_list.length + ')'); } else { bt_select_btn.addClass('bt-disabled btn-default').removeClass('btn-success').prev().addClass('bt-disabled'); - bt_select_val.children().eq(0).html(lan['public'].please_choose + ''); + bt_select_val.children().eq(0).html(lan['public'].please_choose +''); bt_select_val.next().find('li').removeClass('active'); this.batch_active = {}; } @@ -263,116 +262,176 @@ var bt_tools = { * @param {object} rows 渲染的模板 * @return void */ - $reader_column_type:function(item,rows){ - var value = rows[item.fid],event_list = {},className = '',config = [],_that = this; - switch(item.type){ + $reader_column_type: function (item, rows) { + var value = rows[item.fid],event_list = {},className = 'click_' + item.fid + '_' + this.random,config = [],_that = this; + switch (item.type) { case 'text': //普通文本 - config = [value,event_list]; + config = [value, event_list]; break; case 'checkbox': //单选内容 - config = ['',event_list]; + config = ['', event_list]; break; case 'password': - var _copy = '',_eye_open ='',className = 'ico_'+ _that.random +'_',html = '**********' - if(item.eye_open){ - html += ''; - if(!event_list[className+'eye_open']) event_list[className+'eye_open'] = {type:'eye_open_password'}; + var _copy = '', + _eye_open = '', + className = 'ico_' + _that.random + '_', + html = '**********' + if (item.eye_open) { + html += ''; + if (!event_list[className + 'eye_open']) event_list[className + 'eye_open'] = { + type: 'eye_open_password' + }; } - if(item.copy){ - html += '' - if(!event_list[className+'copy']) event_list[className+'copy'] = {type:'copy_password'}; + if (item.copy) { + html += '' + if (!event_list[className + 'copy']) event_list[className + 'copy'] = { + type: 'copy_password' + }; } - config = [html,event_list]; + config = [html, event_list]; break; case 'link': //超链接类型 - className = 'click_'+ item.fid + '_' +this.random; - if(!event_list[className] && item.event) event_list[className] = {event:item.event,type:'rows'}; - config = [''+ value +'',event_list]; + className += '_' + item.fid + if (!event_list[className] && item.event) event_list[className] = { + event: item.event, + type: 'rows' + }; + config = ['' + value + '', event_list]; break; case 'input': //可编辑类型 - blurName = 'blur_'+ item.fid + '_' +this.random; - keyupName = 'keyup_'+ item.fid + '_' +this.random; - if(!event_list[blurName] && item.blur) event_list[blurName] = {event:item.blur,eventType:'blur',type:'rows'}; - if(!event_list[keyupName] && item.keyup) event_list[keyupName] = {event:item.keyup,eventType:'keyup',type:'rows'}; - config = ['',event_list]; + blurName = 'blur_' + item.fid + '_' + this.random; + keyupName = 'keyup_' + item.fid + '_' + this.random; + if (!event_list[blurName] && item.blur) event_list[blurName] = { + event: item.blur, + eventType: 'blur', + type: 'rows' + }; + if (!event_list[keyupName] && item.keyup) event_list[keyupName] = { + event: item.keyup, + eventType: 'keyup', + type: 'rows' + }; + config = ['', event_list]; break; case 'status': // 状态类型 var active = ''; - className = 'click_'+ item.fid + '_' +this.random; - $.each(item.config.list,function(index,items){ - if(items[0] === value) active = items; + $.each(item.config.list, function (index, items) { + if (items[0] === value) active = items; }); - if(!event_list[className] && item.event) event_list[className] = {event:item.event,type:'rows'}; - config = [''+ active[1] +''+ (item.config.icon?'':'') +'',event_list]; + if (!event_list[className] && item.event) event_list[className] = { + event: item.event, + type: 'rows' + }; + config = ['' + active[1] + '' + (item.config.icon ? '' : '') + '', event_list]; break; case 'switch': //开关类型 - // config = ['
        ',event_list]; + var active = '',_random = bt.get_random(5); + active = new Number(value) == true ? 'checked': '' + if (!event_list[className] && item.event) event_list[className] = { + event: item.event, + type: 'rows' + }; + config = ['
        ',event_list]; break; case 'group': var _html = ''; - $.each(item.group,function(index,items){ - className = (item.fid?item.fid:'group') + '_'+ index + '_' + _that.random; - if(items.template){ - var _template = items.template(rows,_that),$template = $(_template); - if($template.length>0){ + $.each(item.group, function (index, items) { + className = (item.fid ? item.fid : 'group') + '_' + index + '_' + _that.random; + var _hide = false; + if (items.template) { + var _template = items.template(rows, _that), + $template = $(_template); + if ($template.length > 0) { _html += $template.addClass(className)[0].outerHTML - }else{ - _html += ''+ _template +''; + } else { + _html += '' + _template + ''; } - }else{ - _html += ''+ items.title +''; + } else { + if(typeof items.hide != "undefined"){ + _hide = typeof items.hide === "boolean"?items.hide:items.hide(rows); + if(typeof _hide != "boolean") return false; + } + _html += '' + items.title + ''; } - _html += ((item.group.length - 1 != index)?' | ':'') - if(!event_list[className] && items.event) event_list[className] = {event:items.event,type:'rows'}; + //当前操作按钮长度等于当前所以值时不向后添加分割 + if(!_hide) _html += ((item.group.length == (index+1)) ? '' : ' | ') + if (!event_list[className] && items.event) event_list[className] = { + event: items.event, + type: 'rows' + }; }); - config = [_html,event_list]; + config = [_html, event_list]; break; default: - config = [value,event_list]; + config = [value, event_list]; break; } return config; }, - + /** + * @description 批量执行程序 + * @param {object} config 配置文件 + * @return void + */ + $batch_success_table:function(config){ + that.$batch_success_table(config); + }, /** * @description 渲染工具条 * @param {object} data 配置参数 * @return void */ - $reader_tootls:function(config){ + $reader_tootls: function (config) { var _that = this,event_list = {}; /** * @description 请求方法 * @param {Function} callback 回调函数 * @returns void */ - function request(active,check_list){ - var loadT = bt.load(active.title +lan.site.executing),batch_config = {}, - list = _that.$get_data_batch_list(active.paramId,check_list); - if(!active.beforeRequest){ + function request(active, check_list) { + var loadT = bt.load(active.title +lan.site.executing), + batch_config = {}, + list = _that.$get_data_batch_list(active.paramId, check_list); + if (!active.beforeRequest) { batch_config[active.paramName] = list.join(','); - }else{ + } else { batch_config[active.paramName] = active.beforeRequest(check_list); } bt_tools.send({ - url:active.url || _that.config.batch.url, - data:$.extend(active.param || {},batch_config) - },function(res){ + url: active.url || _that.config.batch.url, + data: $.extend(active.param || {}, batch_config) + }, function (res) { loadT.close(); - if(!res.status) return false; - if(typeof active.tips === 'undefined' || active.tips){ - var html = ''; - $.each(res.error,function(key,item){ - html += ''+ key +'
        '+ item +'
        '; - }); - $.each(res.success,function(index,item){ - html += ''+ item +'
        '+lan['public'].success+'
        '; - }); - _that.$batch_success_table({title:active.title,th:active.theadName,html:html}); - _that.$refresh_table_list(true); + if (res.status === false && typeof res.success === "undefined"){ + bt_tools.msg(res); + return false; } - if(active.success){ - active.success(res,check_list,_that); + if (typeof active.tips === 'undefined' || active.tips) { + var html = ''; + $.each(res.error, function (key, item) { + html += '' + key + '
        ' + item + '
        '; + }); + $.each(res.success, function (index, item) { + html += '' + item + '
        '+lan['public'].success+'
        '; + }); + _that.$batch_success_table({ + title: active.title, + th: active.theadName, + html: html + }); + if(active.refresh) _that.$refresh_table_list(true); + }else{ + if(!active.success){ + var html = ''; + $.each(check_list,function(index,item){ + html += ''+ item.name +'
        '+((typeof active.theadValue != 'undefined'?active.theadValue[res.status?0:1]:null) || res.msg) +'
        '; + }); + _that.$batch_success_table({title:'Batch' + active.title + 'Success',th:active.theadName,html:html}); + if(active.refresh) _that.$refresh_table_list(true); + } + } + if (active.success) { + active.success(res, check_list, _that); } }); } @@ -380,244 +439,318 @@ var bt_tools = { * @description 执行批量,包含递归批量和自动化批量 * @returns void */ - function execute_batch(active,check_list,success){ - if(active.callback){ - active.callback({ - loadT:0, - config:{}, - check_list:check_list, - bacth_status:true, - start_batch:function(param,callback){ - var _this = this; - if(active.load){ - this.loadT = layer.msg(lan['public'].executeing+ active.title +','+lan['public'].schedule+':0/'+ this.check_list.length +','+lan['public'].please_wait+ (active.clear?'cancel':''),$.extend({icon:16,skin:'batch_tips',shade:.3,time:0},active.clear?{area:'420px'}:{})); - $('#layui-layer'+_this.loadT).on('click','.clear_batch',function(){ - _this.clear_bacth(); - }); + function execute_batch(active, check_list, success) { + // if(active.recursion) 递归方式 + var bacth = { + loadT: 0, + config: {}, + check_list: check_list, + bacth_status: true, + start_batch: function (param, callback) { + var _this = this; + if(typeof param == "undefined") param = {}; + if(typeof param == 'function') callback = param,param = {}; + if (active.load) this.loadT = layer.msg(lan['public'].executeing + active.title + ','+lan['public'].schedule+':0/'+ this.check_list.length +','+lan['public'].please_wait, { + icon: 16, + skin: 'batch_tips', + shade: .3, + time: 0, + area:'400px' + }) + this.config = { + param: param, + url: active.url + }; + this.bacth(callback); + }, + /** + * + * @param {Number} index 递归批量程序 + * @param {Function} callback 回调函数 + * @return void(0) + */ + bacth: function (index, callback) { + var _this = this,param = {}; + if (typeof index === "function" || typeof index === "undefined") callback = index, index = 0; + if (index < this.check_list.length) { + "function" == typeof active.url ? this.config.url = active.url(check_list[index]) : this.config.url = active.url; + if (typeof active.param == "function") { + param = active.param(check_list[index]); + } else { + param = active.param; } - this.config = {param:param,url:active.url}; - this.bacth(callback); - }, - /** - * - * @param {Number} index 递归批量程序 - * @param {Function} callback 回调函数 - * @return void(0) - */ - bacth:function(index,callback){ - var _this = this,param = {}; - if(typeof index === "function") callback = index,index = 0; - if(index < this.check_list.length){ - if(typeof active.param == "function"){ - param = active.param(check_list[index]); - }else{ - param = active.param; - } - this.config.param = $.extend(this.config.param,param); - if(active.paramId)_this.config.param[active.paramId] = _this.check_list[index][active.paramId]; - if(this.config.param['bacth'] && index == this.check_list.length -1){ - delete this.config.param['bacth']; - } - if(!_this.bacth_status) return false; - if(active.load) $('#layui-layer'+_this.loadT).find('.layui-layer-content').html(''+lan['public'].executeing+ active.title +','+lan['public'].schedule+':'+ index +'/'+ _this.check_list.length +','+lan['public'].please_wait+ (active.clear?'cancel':'')); - bt_tools.send({ - url:this.config.url, - data:this.config.param, - bacth:true, - },function(res){ - $.extend(_this.check_list[index],{request:{status:typeof res.status === "boolean"?res.status:false,msg:res.msg || lan['public'].request_error}}); - index++; - _this.bacth(index,callback); - }); - }else{ - if(success) success(); + this.config.param = $.extend(this.config.param, param); + if (typeof active.paramId != 'undefined') _this.config.param[active.paramName || active.paramId] = _this.check_list[index][active.paramId]; + if (typeof active.beforeBacth != 'undefined') this.config.param = $.extend(this.config.param,active.beforeBacth(_this.check_list[index])); + if (this.config.param['bacth'] && index == this.check_list.length - 1) { + delete this.config.param['bacth']; + } + if (!_this.bacth_status) return false; + if (active.load) $('#layui-layer' + _this.loadT).find('.layui-layer-content').html(''+lan['public'].executeing + active.title + ','+lan['public'].schedule+':'+ index +'/'+ _this.check_list.length +','+lan['public'].please_wait+ (active.clear?'cancel':'')); + bt_tools.send({ + url: this.config.url, + data: this.config.param, + bacth: true, + }, function (res) { + $.extend(_this.check_list[index], { + request: { + status: typeof res.status === "boolean" ? res.status : false, + msg: res.msg || lan['public'].request_error + } + }, {requests: res}); + index++; + _this.bacth(index, callback); + }); + } else { + if(success) success(); + if(callback){ callback(this.check_list); - layer.close(this.loadT); } - }, - clear_bacth:function(){ - this.bacth_status = false; + if(active.automatic){ + var html = ''; + for(var i=0;i
        '+ item.request.msg +'
        '; + } + _that.$batch_success_table({title:'Batch' + active.title,th:active.theadName,html:html}); + if(active.refresh) _that.$refresh_table_list(true); + _that.$clear_table_checkbox(); + } layer.close(this.loadT); } - }); - }else{ - if(!active.confirm){ - if(active.confirmVerify){ - bt.show_confirm(active.title+lan['public'].in_bulk,active.title +lan['public'].in_bulk+','+lan['public'].risk_prompt,request) - }else{ + }, + clear_bacth:function(){ + this.bacth_status = false; + layer.close(this.loadT); + } + } + if(active.callback) { + active.callback(bacth); + } else { + if (!active.confirm || active.recursion){ + if (active.confirmVerify){ + bt.show_confirm('批量操作' + active.title + '已选中', '批量' + active.title + ',该操作可能会存在风险,是否继续?', function(index){ + layer.close(index) + if(!active.recursion){ + request(active,check_list); + }else{ + bacth.start_batch(); + } + }); + } else { bt.confirm({ - title:active.title+lan['public'].in_bulk, - msg:active.title+lan['public'].in_bulk+','+lan['public'].risk_prompt, - },function(){ - if(request) request(active,check_list) + title: '批量' + active.title, + msg: '批量' + active.title + ',该操作可能会存在风险,是否继续?', + }, function (index) { + layer.close(index) + if(!active.recursion){ + request(active, check_list); + }else{ + bacth.start_batch(); + } }); } - }else{ - request(active,check_list); + } else { + request(active, check_list); } } } - for(var i=0;i'+ items.title +''; - }else{ - template += '
        \ - \ + $.each(item.list, function (index, items) { + var _btn = item.type + '_' + _that.random + '_' + index, + html = ''; + if (items.type == 'division') { + template += ''; + } else { + if (!items.group) { + template += ''; + } else { + template += '
        \ + \ \
        ' - if(item.list){ - $.each(item.list,function(index,items){ - html += '
      • '+ items[item.key] +'
      • '; - }); + if (item.list) { + $.each(item.list, function (index, items) { + html += '
      • ' + items[item.key] + '
      • '; + }); + } + if (items.init) setTimeout(function () { + items.init(_btn) + }, 400); } - if(items.init) setTimeout(function(){ items.init(_btn) },400); } - if(!event_list[_btn]) event_list[_btn] = {event:items.event,type:'button'}; + if (!event_list[_btn]) event_list[_btn] = { + event: items.event, + type: 'button' + }; }); break; case 'search': this.config.search = item; - var _input = 'search_input_'+this.random,_btn = 'search_btn_'+ this.random; - template = ''; - if(!event_list[_input]) event_list[_input] = {eventType:'keyup',type:'search_input'}; - if(!event_list[_btn]) event_list[_btn] = {type:'search_btn'}; + var _input = 'search_input_' + this.random, + _focus = 'search_focus_' + this.random, + _btn = 'search_btn_' + this.random; + template = ''; + if (!event_list[_input]) event_list[_input] = { + eventType: 'keyup', + type: 'search_input' + }; + if (!event_list[_focus]) event_list[_focus] = { + type: 'search_focus', + eventType: 'focus' + }; + if (!event_list[_btn]) event_list[_btn] = { + type: 'search_btn' + }; break; case 'batch': this.config.batch = item; - var batch_list = [],_html = '',active = item.config; - if(typeof item.config != 'undefined'){ + var batch_list = [], + _html = '', + active = item.config; + if (typeof item.config != 'undefined') { _that.batch_active = active; - $(_that.config.el).on('click','.set_batch_option',function(e){ - var check_list =[]; - for(var i=0;i<_that.checkbox_list.length;i++){ + $(_that.config.el).on('click', '.set_batch_option', function (e) { + var check_list = []; + for (var i = 0; i < _that.checkbox_list.length; i++) { check_list.push(_that.data[_that.checkbox_list[i]]); } - if($(this).hasClass('bt-disabled')){ - layer.tips(_that.config.batch.disabledTips || 'Select batch operation',$(this),{tips: [1,'red'],time: 2000}); + if ($(this).hasClass('bt-disabled')) { + layer.tips(_that.config.batch.disabledTips || 'Select batch operation', $(this), { + tips: [1, 'red'], + time: 2000 + }); return false; } - switch(typeof active.confirm){ + switch (typeof active.confirm) { case 'function': - active.confirm(active,function(param,callback){ - active.param = $.extend(active.param,param); - execute_batch(active,check_list,callback); + active.confirm(active, function (param, callback) { + active.param = $.extend(active.param, param); + execute_batch(active, check_list, callback); }); break; case 'undefined': - execute_batch(active,check_list); + execute_batch(active, check_list); break; case 'object': var config = active.confirm; bt.open({ - title:config.title || 'Batch execute', - area:config.area || '350px', - btn:config.btn || ['Confirm','Cancel'], - content:config.content, - success:function(layero,index){ - config.success(layero,index,active); + title: config.title || 'Batch execute', + area: config.area || '350px', + btn: config.btn || ['Confirm', 'Cancel'], + content: config.content, + success: function (layero, index) { + config.success(layero, index, active); }, - yes:function(index,layero){ - config.yes(index,layero,function(param,callback){ - active.param = $.extend(active.param,param); - request(active,check_list); + yes: function (index, layero) { + config.yes(index, layero, function (param, callback) { + active.param = $.extend(active.param, param); + request(active, check_list); }); } }); break; } }); - }else{ - $.each(item.selectList,function(index,items){ - if(items.group){ - $.each(items.group,function(indexs,itemss){ - batch_list.push($.extend({},items,itemss)); - _html += '
      • '+ itemss.title +'
      • '; + } else { + $.each(item.selectList, function (index, items) { + if (items.group) { + $.each(items.group, function (indexs, itemss) { + batch_list.push($.extend({}, items, itemss)); + _html += '
      • ' + itemss.title + '
      • '; }); delete items.group; - }else { + } else { batch_list.push(items); - _html += '
      • '+ items.title +'
      • '; + _html += '
      • ' + items.title + '
      • '; } }); // 打开批量类型列表 - $(_that.config.el).on('click','.bt_table_select_group .bt_select_value',function(e){ - var _this = this,$parent = $(this).parent(),bt_selects = $parent.find('.bt_selects'),area = $parent.offset(),_win_area = _that.$get_win_area(); - if($parent.hasClass('bt-disabled')){ - layer.tips(_that.config.batch.disabledSelectValue,$parent,{tips: [1,'red'],time: 2000}) + $(_that.config.el).unbind().on('click', '.bt_table_select_group .bt_select_value', function (e) { + var _this = this, $parent = $(this).parent(),bt_selects = $parent.find('.bt_selects'),area = $parent.offset(),_win_area = _that.$get_win_area(); + if ($parent.hasClass('bt-disabled')) { + layer.tips(_that.config.batch.disabledSelectValue, $parent,{tips: [1, 'red'],time: 2000}) return false; } - if($parent.hasClass('active')){ + if ($parent.hasClass('active')) { $parent.removeClass('active'); - }else{ + } else { $parent.addClass('active'); } - if(bt_selects.height() > (_win_area[1] - area.top)){ + if (bt_selects.height() > (_win_area[1] - area.top)) { bt_selects.addClass('top'); - }else{ + } else { bt_selects.removeClass('top'); } - $(document).one('click',function(){ + $(document).one('click', function () { $(_that.config.el).find('.bt_table_select_group').removeClass('active'); return false; }); return false; }); // 选择批量的类型 - $(_that.config.el).on('click','.bt_table_select_group .item',function(e){ - var _text = $(this).text(),_index = $(this).index(); + $(_that.config.el).on('click', '.bt_table_select_group .item', function (e) { + var _text = $(this).text(), + _index = $(this).index(); $(this).addClass('active').siblings().removeClass('active'); - $(_that.config.el +' .bt_select_tips').html(_text + lan['public'].in_bulk+'('+lan.site.have_been_selected+ _that.checkbox_list.length +')'); + $(_that.config.el + ' .bt_select_tips').html('Batch execute' + _text + '('+lan.site.have_been_selected + _that.checkbox_list.length + ')'); _that.batch_active = batch_list[_index]; - if(!_that.checked) $('.bt_table_select_group').removeClass('active'); + if (!_that.checked) $('.bt_table_select_group').removeClass('active'); }); // 执行批量操作 - $(_that.config.el).on('click','.set_batch_option',function(e){ - var check_list = [],active = _that.batch_active; - if($(this).hasClass('bt-disabled')){ - layer.tips(_that.config.batch.disabledSelectValue,$(this),{tips: [1,'red'],time: 2000}); + $(_that.config.el).on('click', '.set_batch_option', function (e) { + var check_list = [], + active = _that.batch_active; + if ($(this).hasClass('bt-disabled')) { + layer.tips(_that.config.batch.disabledSelectValue, $(this), { + tips: [1, 'red'], + time: 2000 + }); return false; } - for(var i=0;i<_that.checkbox_list.length;i++){ + for (var i = 0; i < _that.checkbox_list.length; i++) { check_list.push(_that.data[_that.checkbox_list[i]]); } - if(JSON.stringify(active) === '{}'){ + if (JSON.stringify(active) === '{}') { var bt_table_select_group = $(_that.config.el + ' .bt_table_select_group'); - layer.tips(lan['public'].select_opt_type,bt_table_select_group,{tips:[1,'red'],time: 2000}); - bt_table_select_group.css('border','1px solid red'); - setTimeout(function(){ + layer.tips(lan['public'].select_opt_type, bt_table_select_group, { + tips: [1, 'red'], + time: 2000 + }); + bt_table_select_group.css('border', '1px solid red'); + setTimeout(function () { bt_table_select_group.removeAttr('style'); - },2000); + }, 2000); return false; } - switch(typeof active.confirm){ + switch (typeof active.confirm) { case 'function': - active.confirm(active,function(param,callback){ - active.param = $.extend(active.param,param); - execute_batch(active,check_list,callback); + active.confirm(active, function (param, callback) { + active.param = $.extend(active.param, param); + execute_batch(active, check_list, callback); }); break; case 'undefined': - execute_batch(active,check_list); + execute_batch(active, check_list); break; case 'object': var config = active.confirm; bt.open({ - title:config.title || lan['public'].bulk_opt, - area:config.area || '350px', - btn:config.btn || [lan['public'].confirm,lan['public'].cancel], - content:config.content, - success:function(layero,index){ - config.success(layero,index,active); + title: config.title || lan['public'].bulk_opt, + area: config.area || '350px', + btn: config.btn || [lan['public'].confirm,lan['public'].cancel], + content: config.content, + success: function (layero, index) { + config.success(layero, index, active); }, - yes:function(index,layero){ - config.yes(index,layero,function(param,callback){ - active.param = $.extend(active.param,param); - request(active,check_list); + yes: function (index, layero) { + config.yes(index, layero, function (param, callback) { + active.param = $.extend(active.param, param); + request(active, check_list); }); } }); @@ -625,89 +758,46 @@ var bt_tools = { } }); } - // template = '
        '+lan['public'].select_opt_type+'
          '+ _html +'
        '; - template = '
        '+ (typeof item.config != 'undefined'?'':'
        '+lan['public'].select_opt_type+'
          '+ _html +'
        ') +'
        '; + template = '
        ' + (typeof item.config != 'undefined' ? '' : '
        '+lan['public'].select_opt_type+'
          ' + _html + '
        ') + '
        '; break; - // case 'batch_btn': - // _that.batch = item; - // $(_that.config.el).on('click','.set_batch_option',function(e){ - // var check_list = [],active = {},index = $(this).index()-1; - // _that.batch_active = _that.batch.selectList[index]; - // active = _that.batch_active; - // if($(this).hasClass('bt-disabled')){ - // layer.tips(_that.batch.disabledSelectValue,$(this),{tips: [1,'red'],time: 2000}); - // return false; - // } - // for(var i=0;i<_that.checkbox_list.length;i++){ - // check_list.push(_that.data[_that.checkbox_list[i]]); - // } - // switch(typeof active.confirm){ - // case 'function': - // active.confirm(active,function(param,callback){ - // active.param = $.extend(active.param,param); - // execute_batch(active,check_list,callback); - // }); - // break; - // case 'undefined': - // execute_batch(active,check_list); - // break; - // case 'object': - // var config = active.confirm; - // bt.open({ - // title:config.title || lan['public'].exec, - // area:config.area || '350px', - // btn:config.btn || [lan['public'].confirm,lan['public'].cancel], - // content:config.content, - // success:function(layero,index){ - // config.success(layero,index,active); - // }, - // yes:function(index,layero){ - // config.yes(index,layero,function(param,callback){ - // active.param = $.extend(active.param,param); - // request(active,check_list); - // }); - // } - // }); - // break; - // } - // }); - // template = '
        '; - // break; case 'page': this.config.page = item; - var pageNumber = bt.get_cookie(this.config.cookiePrefix+'_'+ this.config.page.numberParam); - if(this.config.cookiePrefix && pageNumber) this.config.page.number = pageNumber; - template = this.$reader_page(this.config.page,'
        1 '+lan.public_backup.total+'
        '); - break; + var pageNumber = bt.get_cookie('page_number'); + if (this.config.cookiePrefix && pageNumber) this.config.page.number = pageNumber; + template = this.$reader_page(this.config.page,'
        1'+lan.public_backup.total+'
        '); + break; } - if(template){ - var tools_group = $(_that.config.el + ' .tootls_'+ item.positon[1]); - if(tools_group.length){ - var tools_item = tools_group.find('.pull-'+ item.positon[0]); + if (template) { + var tools_group = $(_that.config.el + ' .tootls_' + item.positon[1]); + if (tools_group.length) { + var tools_item = tools_group.find('.pull-' + item.positon[0]); tools_item.append(template); - }else{ - var tools_group_elment = '
        '+ (item.positon[0]=='left'?template:'') +'
        '+ (item.positon[0]=='right'?template:'') +'
        '; - if(item.positon[1] === 'top'){ + } else { + var tools_group_elment = '
        ' + (item.positon[0] == 'left' ? template : '') + '
        ' + (item.positon[0] == 'right' ? template : '') + '
        '; + if (item.positon[1] === 'top') { $(_that.config.el).append(tools_group_elment); - if($(_that.config.el +' .divtable').length === 0) $(_that.config.el).append('
        '); - }else{ - if($(_that.config.el +' .divtable').length === 0) $(_that.config.el).append('
        '); + if ($(_that.config.el + ' .divtable').length === 0) $(_that.config.el).append('
        '); + } else { + if ($(_that.config.el + ' .divtable').length === 0) $(_that.config.el).append('
        '); $(_that.config.el).append(tools_group_elment); } } } } - if(!this.init) this.$event_bind(event_list); + if (!this.init) this.$event_bind(event_list); }, + $clear_table_checkbox:function(){ + $(this.config.el).find('.bt_table .cust—checkbox').removeClass('selected active'); + }, /** * @description 获取数据批量列表 * @param {string} 需要获取的字段 * @return {array} 当前需要批量列表 */ - $get_data_batch_list:function(fid,data){ + $get_data_batch_list: function (fid, data) { var arry = []; - $.each(data || this.data,function(index,item){ + $.each(data || this.data, function (index, item) { arry.push(item[fid]) }); return arry; @@ -718,688 +808,41 @@ var bt_tools = { * @param {object} config 配置文件 * @param {object} page 分页 * @return void - */ - $reader_page:function(config,page){ - var _that = this,$page = $(page),template = '',eventList = {}; - $page.find('a').addClass('page_link_'+ this.random); + */ + $reader_page:function (config,page){ + if(typeof page === 'number'){ + page = this.$custom_page(page); + } + var _that = this,$page = $(page), template = '', eventList = {}; + $page.find('a').addClass('page_link_' + this.random); template += $page.html(); - if(config.numberStatus){ - var className = 'page_select_'+ this.random; - template += ''; + $.each(config.numberList, function(index, item) { + template += ''; }); template += ''; - eventList[className] = {eventType:"change",type:'page_select'}; + eventList[className] = {eventType: "change",type: 'page_select'}; } - if(config.jump){ - var inputName = 'page_jump_input-'+ this.random; - var btnName = 'page_jump_btn_'+ this.random; - template += '
        '+lan['public'].jump_to_page+'
        ' - eventList[inputName] = {eventType:'keyup',type:'page_jump_input'}; - eventList[btnName] = {type:'page_jump_btn'}; + if (config.jump) { + var inputName = 'page_jump_input_' + this.random; + var btnName = 'page_jump_btn_' + this.random; + template += '
        '+lan['public'].jump_to_page+'
        ' + eventList[inputName] = { + eventType: 'keyup', + type: 'page_jump_input' + }; + eventList[btnName] = { + type: 'page_jump_btn' + }; } - eventList['page_link_'+ this.random] = {type:'cut_page_number'}; - _that.config.page.total = parseInt($page.find('.Pcount').html().match(/([0-9]*)/g)[1]); + eventList['page_link_' + this.random] = { + type: 'cut_page_number' + }; + _that.config.page.total = $page.length == 0?0:(typeof page == "number"?page:parseInt($page.find('.Pcount').html().match(/([0-9]*)/g)[1])); _that.$event_bind(eventList); - return '
        '+ template +'
        '; - }, - - /** - * @deprecated 动态处理合并行内,css样式 - * @param {object} rows 当前行数据 - * @return {stinrg} className class类名 - * @return void - */ - $dynamic_merge_style:function(column,index){ - var str = ''; - $.each(column,function(key,item){ - switch(key){ - case 'align': - str += 'text-align:'+ item +';'; - break; - case 'width': - str += 'width:'+ (typeof item == 'string'?item:item +'px') +';'; - break; - case 'style': - str += item; - break; - case 'minWidth': - str += 'min-width:'+ (typeof item == 'string'?item:item +'px') +';'; - break; - case 'maxWidth': - str += 'max-width:'+ (typeof item == 'string'?item:item +'px') +';'; - break; - } - }); - return {index:index,css:str}; - }, - - /** - * @description 事件绑定 - * @param {array} eventList 事件列表 - * @return void - */ - $event_bind:function(eventList){ - var _that = this; - $.each(eventList,function(key,item){ - if(_that.event_list[key] && _that.event_list[key].eventType === item.eventType) return true; - _that.event_list[key] = item; - $(_that.config.el).on(item.eventType || 'click','.'+ key,function(ev){ - var index = $(this).parents('tr').index(),data1 = $(this).data(),arry = []; - switch(item.type){ - case 'rows': - _that.event_rows_model = { - el:$(this), - model:_that.config.column[$(this).parents('td').index()], - rows:_that.data[index], - index:index - } - arry = [_that.event_rows_model.rows,_that.event_rows_model.index,ev,key,_that]; - break; - case 'sort': - var model = _that.config.column[data1.index]; - if($(this).hasClass('sort-active')) $('.sort_'+ _that.random + ' .sort-active').data({'sort':'desc'}); - $('.sort_'+ _that.random).removeClass('sort-active').find('.glyphicon').removeClass('glyphicon-triangle-top').addClass('glyphicon-triangle-bottom'); - $(this).addClass('sort-active'); - if(data1.sort == 'asc'){ - $(this).data({'sort':'desc'}); - $(this).find('.glyphicon').removeClass('glyphicon-triangle-top').addClass('glyphicon-triangle-bottom'); - }else{ - $(this).data({'sort':'asc'}); - $(this).find('.glyphicon').removeClass('glyphicon-triangle-bottom').addClass('glyphicon-triangle-top'); - } - _that.config.sort = _that.config.sortParam({name:model.fid,sort:data1.sort}) - _that.$refresh_table_list(true); - break; - case 'checkbox': - var all = $(_that.config.el + ' [data-checkbox="all"]'),checkbox_list = $(_that.config.el +' tbody .checkbox_'+_that.random); - if(data1.checkbox == undefined){ - if(!$(this).hasClass('active')){ - $(this).addClass('active'); - _that.checkbox_list.push(index); - if(_that.data.length === _that.checkbox_list.length){ - all.addClass('active').removeClass('selected') - }else if(_that.checkbox_list.length > 0){ - all.addClass('selected'); - } - }else{ - $(this).removeClass('active'); - _that.checkbox_list.splice(_that.checkbox_list.indexOf(index),1); - if(_that.checkbox_list.length > 0){ - all.addClass('selected').removeClass('active'); - }else{ - all.removeClass('selected active'); - } - } - }else{ - if(_that.checkbox_list.length === _that.data.length){ - _that.checkbox_list = []; - checkbox_list.removeClass('active selected').next().prop('checked','checked') - all.removeClass('active'); - }else{ - checkbox_list.each(function(index,item){ - if(!$(this).hasClass('active')){ - $(this).addClass('active').next().prop('checked','checked'); - _that.checkbox_list.push(index); - } - }); - all.removeClass('selected').addClass('active'); - } - } - _that.$set_batch_view(); - break; - case 'button': - arry.push(ev,_that); - break; - case 'search_focus': - var search_tips = $(_that.config.el+' .bt_search_tips'); - if($(_that.config.el + ' .bt_search_tips').length > 0){ - search_tips.remove(); - } - break; - case 'search_input': - if(ev.keyCode == 13){ - $(_that.config.el +' .search_btn_'+ _that.random).click(); - return false; - } - break; - case 'search_btn': - var _search = $(_that.config.el+' .search_input'),val = $(_that.config.el+' .search_input').val(); - _that.config.search.value = val; - _search.append('
        '+ val +'
        '); - _that.$refresh_table_list(true); - break; - case 'page_select': - var limit = parseInt($(this).val()); - _that.config.page.number = limit; - _that.config.page.page = 1; - _that.$refresh_table_list(true); - return false; - break; - case 'page_jump_input': - if(ev.keyCode == 13){ - $(_that.config.el +' .page_jump_btn_'+ _that.random).click(); - $(this).focus(); - } - return false; - break; - case 'page_jump_btn': - var jump_page = parseInt($(_that.config.el +' .page_jump_input-'+ _that.random).val()),max_number = Math.ceil(_that.config.page.total/ _that.config.page.number); - if(jump_page > max_number) jump_page = _that.config.page.page; - _that.config.page.page = jump_page; - _that.$refresh_table_list(true); - break; - case 'cut_page_number': - var page = parseInt($(this).attr('href').match(/([0-9]*)$/)[0]) - _that.config.page.page = page; - _that.$refresh_table_list(true); - return false; - break; - case 'eye_open_password': - if($(this).hasClass('glyphicon-eye-open')){ - $(this).addClass('glyphicon-eye-close').removeClass('glyphicon-eye-open'); - $(this).prev().text(_that.data[index].password); - }else{ - $(this).addClass('glyphicon-eye-open').removeClass('glyphicon-eye-close'); - $(this).prev().html('**********'); - } - return false; - break; - case 'copy_password': - bt.pub.copy_pass(_that.data[index].password); - return false; - break; - } - if(item.event) item.event.apply(this,arry); - }); - }); - }, - - /** - * @description 样式绑定 - * @param {array} style_list 样式列表 - * @return void - */ - $style_bind:function(style_list,status){ - var str = '',_that = this; - $.each(style_list,function(index,item){ - if(item.css != ''){ - if(!item.className){ - str += _that.config.el +' thead th:nth-child('+ (item.index + 1) +'),'+ _that.config.el+' tbody tr td:nth-child'+ (item.span?' span':'') +'('+ (item.index + 1) +'){'+ item.css +'}' - }else{ - str += item.className + '{'+ item.css +'}'; - } - } - }); - if($('#bt_table_'+ _that.random ).length == 0) $(_that.config.el).append(''); - }, - - /** - * @deprecated 获取WIN高度或宽度 - * @returns 返回当期的宽度和高度 - */ - $get_win_area:function(){ - return [window.innerWidth,window.innerHeight]; - }, - - /** - * @description 请求数据, - * @param {object} param 参数和请求路径 - * @return void - */ - $http:function(success){ - var param = {},config = this.config,_page = config.page,_search = config.search,_sort = config.sort || {}; - if(_page){ - param[_page.numberParam] = _page.number,param[_page.pageParam] = _page.page; - if(this.config.cookiePrefix) bt.set_cookie(this.config.cookiePrefix+'_'+ _page.numberParam,_page.number); - } - if(_search) param[_search.searchParam] = _search.value; - if(this.config.beforeRequest) config.param = this.config.beforeRequest(config.param); - bt_tools.send({ - url:config.url, - data:$.extend(config.param,param,_sort), - },function(res){ - if(config.dataFilter){ - var data = config.dataFilter(res); - if(success) success(data.data,data.page); - }else { - if(success) success(res.data,res.page); - } - }); - } - } - return new ReaderTable(config); - }, - /** - * @description 选择文件目录 - * @param {object|string} data 当前 {path:[string] 选择路径,title:[string] title标题,ext:[array]限制的文件类型},或选择目录地址,可以为空,为空这默认使用当前目录作为选择目录 - * @param {function} 回调函数,选择完成后的操作 - * @return void - */ - select_file:function(data,callback){ - if(typeof data === 'string') data = {path:data,type:2,title:lan['public'].select_fileordir,ext:[],limit:''}; - if(typeof data === 'function') type = callback,callback = data,data = {path:bt.get_cookie('Path')}; - if(typeof type !== 'number') type = 0; - var that = this,select_file = { - type:type, - type_tips:type == 0?lan['public'].dir:(type == 1?lan.site.file:lan['public'].fileordir), - select_list:[], - select_path:bt.get_cookie('Path') || (bt.os == 'Windows'?setup_path:'/www/wwwroot'), - select_config:{}, - scroll_width:0, - reader_view:function(){ - var _this = this; - this.scroll_width = select_file.getScrollbarWidth(); - layer.open({ - type:1, - title: data.title || (lan['public'].select + _this.type_tips), - shadeClose:false, - closeBtn:1, - area:['650px','550px'], - content:'
        '+lan['public'].name+''+lan['public'].modify_time+''+lan.site.redirect_type+'
        文件修改时间类型
        '+ _this.type_tips +'名:
        ', - success:function(){ - _this.evnet_bind(); - _this.render_file_list(function(){ - _this.set_path_width(); - }); - $('.select_list_thaed').css('right',_this.scroll_width+'px'); - } - }); - }, - /** - * @description 事件绑定 - */ - evnet_bind:function(){ - var _this = this; - $('#select_dir_list').on('click','tr',function(){ - var index = $(this).data('index'); - $(this).addClass('active').siblings().removeClass('active'); - $('[name="select_name"]').val(_this.select_list[index].filename); - }); - $('.select_list_body').on('scroll',function(e){ - var top = $(this).scrollTop(),left = $(this).scrollLeft(); - if(top > 0){ - $('.select_list_shadow').show(); - }else{ - $('.select_list_shadow').hide(); - } - if(left >= 0){ - $('.select_list_thaed').css('left',(4-left ) +'px'); - } - e.stopPropagation(); - e.preventDefault(); - }); - $('.file_path_refresh').on('click',function(){ - _this.render_file_list(); - }); - $('.select_dir_list thead th .icon-drag').on('mousedown',function(ev){ - var x = ev.clientX,th = $(this).parent(),th_index = th.index(),th_width = th[0].clientWidth,min_width = parseInt($(this).parent().attr('data-min')),timeOut = null; - if(ev.which === 1){ - var document_mousemove = function(e){ - var move_x = e.clientX,offset_x = move_x - x,_width = th_width + offset_x; - timeOut = setTimeout(function(){ - _this.set_select_width(th_index,'width:'+_width+'px'); - },0); - }, - document_mouseup = function(){ - $(this).unbind(document_mousemove); - $(this).unbind(document_mouseup); - }; - $(document).on('mousemove',document_mousemove).one('mouseup',function(){ - $(this).unbind(document_mousemove); - $(this).unbind(this); - }) - } - ev.stopPropagation(); - }); - $('.select_dir_list thead th>span').on('click',function(ev){ - var th = $(this).parent(),type = th.data('type'),is_active = th.hasClass('active'),is_sort = th.hasClass('sort'); - if(is_active){ - th.addClass('sort').removeClass('active'); - }else if(is_sort){ - th.addClass('active').removeClass('sort'); - }else{ - th.addClass('active'); - } - th.siblings().removeClass('active sort'); - _this.render_file_list({sort:type,reverse:is_active?1:0}); - ev.stopPropagation(); - }); - $('.search_path_views input').on('keyup',function(){ - var val = $(this).val(); - }); - }, - /** - * @description 设置当前路径宽度 - * @param {} - */ - set_path_width:function(width){ - var _width = 0,_dir_view = $('.file_dir_view')[0].offsetWidth,_item = $('.file_dir_view .file_dir_item'),_arry = [],_width =0; - $('.file_dir_view .file_dir_item').each(function(){ - _arry.push($(this)[0].offsetWidth); - }); - var arry = _arry.reverse(); - for(var i=0;i -1?('本地磁盘('+ item.path.match(/[A-Z]+/)[0])+ ':)':item.path) - mount_html += '
        '+ - ''+ - ''+ name +''+ - '
        '; - }); - $.each(arry,function(index,item){ - var mtime = bt.format_data(item.mtime),type = (item.type == 'dir'?'文件夹':'文件'); - dir_html += '\ - '+ item.filename +'\ - '+ mtime +'\ - '+ (item.type == 'dir'?'文件夹':'文件') +'\ - '; - }); - $('#select_mount_list').html(mount_html); - $('#select_dir_list').html(dir_html); - if(callback) callback(rdata); - }); - }, - /** - * @description 渲染路径列表 - * @param {Function} callback 回调函数 - * @return void - */ - render_path_list:function(path,callback){ - var html = '
        ', path_before = '',dir_list = path.split("/").splice(1),first_dir = path.split("/")[0]; - if(bt.os === 'Windows'){ - if(dir_list.length == 0) dir_list = []; - dir_list.unshift('本地磁盘('+ first_dir +')'); - }else{ - if(path == '/') dir_list = []; - dir_list.unshift('根目录'); - } - for(var i = 0; i < dir_list.length; i++){ - path_before += '/' + dir_list[i]; - if (i == 0) path_before = ''; - html += '
        \ - ' + dir_list[i] + '\ - \ - \ -
        '; - } - $('.path_input').val('').attr('data-path',path); - var file_dir_view = $('#select_file_directory .file_path_input .file_dir_view'); - file_dir_view.html(html); - if(callback) callback(path); - }, - - /** - * @description 设置标题宽度 - * @param {string} th_index 表头序列号 - * @param {string} style class样式 - */ - set_select_width:function(th_index,style){ - var _this = this,className = '.select_dir_list tbody td:nth-child('+ (th_index+1) +')>span,.select_dir_list thead th:nth-child('+ (th_index+1) +')>span',style_index = $('#th-index-'+th_index); - if(style_index.length > 0){ - style_index.html(className +'{'+ style +'}'); - }else{ - $('#select_file_style').append(''); - } - }, - /** - * @description 样式转换文件样式 - * @param {string} style 样式 - */ - cut_style_object:function(style){ - var object = {},arry = style.split(';'); - return object; - }, - /** - * @description 获取滚动条宽度 - */ - getScrollbarWidth:function() { - var odiv = document.createElement('div'),//创建一个div - styles = { - width: '100px', - height: '100px', - overflowY: 'scroll'//让他有滚动条 - }, i, scrollbarWidth; - for (i in styles) odiv.style[i] = styles[i]; - document.body.appendChild(odiv);//把div添加到body中 - scrollbarWidth = odiv.offsetWidth - odiv.clientWidth;//相减 - odiv.remove();//移除创建的div - return scrollbarWidth;//返回滚动条宽度 - } - } - select_file.reader_view(); - }, - /** - * @description 渲染Form表单 - * @param {*} config - * @return 当前实例对象 - */ - form: function (config) { - var _that = this; - function ReaderForm(config) { - this.config = config; - this.data = config.data || {}; - this.$load(); - } - ReaderForm.prototype = { - element: null, - style_list: [], // 样式列表 - event_list: {}, // 事件列表,已绑定事件 - event_type: ['click', 'event', 'focus', 'keyup', 'blur', 'change'], - hide_list: [], - form_element: {}, - form_config: {}, - random: bt.get_random(5), - $load: function () { - var that = this; - if (that.el) { - this.$reader_content(function () { - that.$event_bind(); - }); - } - }, - - /** - * @description 渲染Form内容 - * @param {Function} callback 回调函数 - */ - $reader_content: function (callback) { - var that = this, - html = '', - _content = '', - event_list = {}; - $.each(that.config.form, function (index, item) { - html += that.$reader_content_row(index, item); - }); - that.element = $('
        ' + html + '
        '); - _content = $('
        '); - _content.append(that.element); - if (callback) callback(); - return _content[0].outerHTML; - }, - - /** - * @description 渲染行内容 - * @param {object} data Form数据 - * @param {number} index 下标 - * @return {string} HTML结构 - */ - $reader_content_row: function (index, data) { - var that = this,help = data.help,labelWidth = this.config.formLabelWidth || data.formLabelWidth; - return '
        ' + - (data.label ? '' + data.label + '' : '') + - '
        ' + - that.$reader_form_element(data.group, index) + - (help ? ('
        ' + help.list.join('
        ') + '
        ') : '') + - '
        ' + - '
        '; - }, - - /** - * @description 渲染form类型 - * @param {object} data 表单数据 - * @param {number} index 下标 - * @return {string} HTML结构 - */ - $reader_form_element: function (data, index) { - var that = this,html = ''; - if (!Array.isArray(data)) data = [data]; - $.each(data, function (key, item) { - item.find_index = index; - html+= that.$reader_form_find(item); - that.form_config[item.name] = item; - }); - return html; - }, - /** - * @descripttion 渲染单个表单元素 - * @param {Object} item 配置 - * @return: viod - */ - $reader_form_element:function(data,index){ - var that = this,config = [],html = '',event_list = {}; - if(!Array.isArray(data)) data = [data]; - $.each(data,function(key,item){ - var style = that.$reader_style(item.style) + _that.$verify(item.width,'width','style'), - attribute = that.$verify_group(item,['name','value','placeholder','disabled','readonly','autofocus','autocomplete','min','max']), - event_group = that.$create_event_config(item), - eventName = ''; - html += item.label?''+ item.label +'':''; - if(typeof item['name'] !== "undefined") event_list[item.name] = event_group; - switch(item.type){ - case 'text': // 文本选择 - case 'checkbox': // 复选框 - case 'password': // 密码 - case 'radio': // 单选框 - case 'number': // 数字 - var _event = 'event_'+ item.name +'_'+ that.random,is_checkbox = item.type === 'checkbox'?true:false; - if(item.type === 'radio'){ - $.each(item.label_tips,function(index,items){ - if(item.block) html += '
        '; - html += ''; - html += ''; - if(item.block) html += '
        '; - }) - }else{ - if(item.block) html += '
        '; - html += ''; - if(item.type == 'checkbox') html += ''; - if(item.block) html += '
        '; - that.event_list[_event] = {'click':{event:item.event}} - } - if(item.icon){ - html += ''; - event_list[item.name+'_icon'] = {'click':{type:'select_path',config:item}}; - } - break; - case 'textarea': - html += ''; - $.each(['blur','focus','input'],function(index,items){ - if(item.tips){ - var added = null; - switch(items){ - case 'blur': - added = function(ev,item){ - if($(this).val() === '') $(this).next().show(); - layer.close(item.tips.loadT); - } - break; - case 'focus': - added = function(ev,item){ - $(this).next().hide(); - item.tips.loadT = layer.tips(tips,$(this),{tips:[1,'#20a53a'],time:0,area:$(this).width()}) - } - break; - } - } - event_list[item.name][items]?(event_list[item.name][items]['added'] = added):(event_list[item.name][items] = {type:item.type,cust:false,event:item[items],added:added}); - }); - if(item.tips){ - var tips = ''; - if(typeof item.tips.list === "undefined"){ - tips = item.tips.text; - }else{ - tips = item.tips.list.join('
        '); - } - html += '
        '+ tips +'
        '; - event_list[item.name+'_tips'] = {'click':{type:'textarea_tips',config:item}}; - } - break; - case 'select': - html += that.$reader_select(item,style,attribute,index); - that.event_list['custom_select'] = {'click':{type:'custom_select',children:'.bt_select_value'}}; - that.event_list['custom_select_item'] = {'click':{type:'custom_select_item',children:'li.item'}}; - break; - case 'link': - eventName = 'event_' + bt.get_random(6); - html += ''+ item.title +''; - that.event_list[eventName] = {'click':{type:'link_event',event:item.event}} - break; - case 'help': - var _html = ''; - $.each(item.list,function(index,items){ - _html += '
      • '+items +'
      • '; - }) - html += '
          '+ _html +'
        '; - break; - } - that.form_config[item.name] = item; - }); - $.extend(that.event_list,event_list); - return html; - }, - /** - * @description 创建事件配置 - * @param {object} item 行内配置 - * @return {object} 配置信息 - */ - $create_event_config: function (item) { - var config = {}; - if (typeof item['name'] === "undefined") return {}; - $.each(this.event_type, function (key, items) { - if (item[items]) { - config[(items === 'event' ? 'click' : items)] = { - type: item.type, - event: item[items], - cust: (['select', 'checkbox', 'radio'].indexOf(item.type) > -1 ? true : false), - config: item - }; - } - }); - return config; + return '
        ' + template + '
        '; }, /** * @description 渲染样式 @@ -1416,368 +859,1012 @@ var bt_tools = { return style; }, /** - * @descripttion 局部刷新form表单元素 - * @param {String} name 需要刷新的元素 - * @param {String} name 元素新数据 - * @return: viod - */ - $local_refresh:function(name,config){ - this.element.find('[data-name='+name+']').replaceWith(this.$reader_form_find(config)) - }, - /** - * @description 渲染下拉,内容方法 - */ - $reader_select: function (item, style, attribute, index) { - var that = this, - list = '', - option = '', - active = {}; - if (!Array.isArray(item.list)) { - var config = item.list; - bt_tools.send({ - url: config.url, - data: config.param || {} - }, function (res) { - if (res.status !== false) { - item.list = item.list.dataFilter ? item.list.dataFilter(res) : res; - that.$replace_render_content(index); - } else { - bt.msg(res); - } - }); + * @description 自定义分页 + * @param {} + */ + $custom_page:function(total){ + var html = '
        ', + config = this.config.page, + page = Math.ceil(total / config.number), + tmpPageIndex = 0; + if(config.page > 1 && page > 1){ + html += '首页上一页'; } - $.each(item.list, function (key, items) { - try{ - if (items.value === item.value) { - active = items; - } else { - active = item.list[0]; - } - }catch(er){} - list += '
      • ' + items.title + '
      • '; - option += ''; - }); - if (!active.title && Array.isArray(item.list)) active = item.list[0]; - return '
        ' + - '' + (active.title || 'Getting data...') + '' + - '
          ' + (list || '') + '
        ' + - '' + (option || '') + '' + - '
        '; + if(page <= 10){ + for (var i = 1; i <= page; i++){ + i == config.page ? html += '' + i + "": html += '' + i + ""; + } + }else if (config.page < 10) { + for (var i = 1; i <= 10; i++) i == config.page ? html += '' + i + "": html += '' + i + ""; + html += "..." + } else if (page - config.page < 7) { + page - 7 > 1 && (html += '1', html += "..."); + for (var i = page - 7; i <= page; i++) i == config.page ? html += '' + i + "": (html += 1 == i ? "...": '' + i + "") + } else { + 0 == tmpPageIndex && (tmpPageIndex = config.page), + (tmpPageIndex <= config.page - 5 || tmpPageIndex >= config.page + 5) && (tmpPageIndex = config.page), + html += '1', + html += "..."; + for (var i = tmpPageIndex - 3; i <= tmpPageIndex + 3; i++) i == config.page ? html += '' + i + "": html += '' + i + ""; + html += "...", + html += '' + page + "" + } + return page > 1 && config.page < page && (html += '下一页尾页'), + html += '共' + total + "条
        " }, - /** - * @description 替换渲染内容 - */ - $replace_render_content: function (index) { - var that = this, - config = this.config.form[index]; - $('[data-form=' + that.random + ']').find('.line:eq(' + index + ')').replaceWith(that.$reader_content_row(index, config)); - }, /** - * @description 事件绑定功能 - * @param {Array} eventList 事件列表 - * @param {Function} callback 回调函数 + * @deprecated 动态处理合并行内,css样式 + * @param {object} rows 当前行数据 + * @return {stinrg} className class类名 * @return void */ - $event_bind: function (eventList, callback) { - var that = this,_event = {}; - that.element = $(typeof eventList === 'object' ? that.element : ('[data-form=' + that.random + ']')); - if (typeof eventList === 'undefined') _event = that.event_list; - console.log(_event); - $.each(_event, function (key, item) { - if ($.isEmptyObject(item)) return true; - $.each(item, function (keys, items) { - var childNode = ''; - if (typeof items.cust === "boolean") { - childNode = '[' + (items.cust ? 'data-' : '') + 'name=' + key + ']'; - } else { - childNode = '.' + key; - } - (function (items, key) { - if (items.onEvent === false) { - switch (items.type) { - case 'input_checked': - $(childNode).on(keys != 'event' ? keys : 'click', function (ev) { - items.event.apply(this, [ev, that]); - }); - break; + $dynamic_merge_style: function (column, index) { + var str = ''; + $.each(column, function (key, item) { + switch (key) { + case 'align': + str += 'text-align:' + item + ';'; + break; + case 'width': + str += 'width:' + (typeof item == 'string' ? item : item + 'px') + ';'; + break; + case 'style': + str += item; + break; + case 'minWidth': + str += 'min-width:' + (typeof item == 'string' ? item : item + 'px') + ';'; + break; + case 'maxWidth': + str += 'max-width:' + (typeof item == 'string' ? item : item + 'px') + ';'; + break; + } + }); + return { + index: index, + css: str + }; + }, + + /** + * @description 事件绑定 + * @param {array} eventList 事件列表 + * @return void + */ + $event_bind: function (eventList){ + var _that = this; + $.each(eventList, function (key, item) { + if (_that.event_list[key] && _that.event_list[key].eventType === item.eventType) return true; + _that.event_list[key] = item; + $(_that.config.el).on(item.eventType || 'click', '.' + key, function (ev) { + var index = $(this).parents('tr').index(), + data1 = $(this).data(), + arry = []; + switch (item.type) { + case 'rows': + _that.event_rows_model = { + el: $(this), + model: _that.config.column[$(this).parents('td').index()], + rows: _that.data[index], + index: index } - return true; - } else { - that.element.on(keys != 'event' ? keys : 'click', items.children ? items.children : childNode, function (ev) { - var form = that.$get_form_element(true), value = that.$get_form_value(), config = that.form_config[key]; - switch (items.type) { - case 'textarea_tips': - $(this).hide().prev().focus(); - break; - case 'custom_select': - if ($(this).parent().hasClass('bt-disabled')) return false; - var select_value = $(this).next(); - if (!select_value.hasClass('show')) { - $('.bt_select_list').removeClass('show'); - select_value.addClass('show'); - } else { - select_value.removeClass('show'); - } - $(document).click(function () { - that.element.find('.bt_select_list').removeClass('show'); - $(this).unbind('click'); - return false; - }); - return false; - break; - case 'custom_select_item': - config = that.form_config[$(this).parents('.bt_select_updown').attr('data-name')], item_config = config.list[$(this).index()] - if ($(this).hasClass('disabled')) { - $(this).parent().removeClass('show'); - if (item_config.tips) layer.msg(item_config.tips, { - icon: 2 - }); - return true; - } - if (!$(this).hasClass('active') && !$(this).hasClass('disabled')) { - $(this).parent().prev().find('.bt_select_content').text($(this).text()); - $(this).addClass('active').siblings().removeClass('active'); - $(this).parent().next().val(item_config.value.toString()); - $(this).parent().removeClass('show'); - $(this).parent().next().trigger("change"); - } - break; - case 'select_path': - bt.select_path('event_' + $(this).prev().attr('name') + '_' + that.random,items.config.icon.select||""); - break; - case 'checkbox': - var checked = $(this).is(':checked'); - if(checked){ - $(this).prev().addClass('active'); - }else{ - $(this).prev().removeClass('active'); - } - break; - } - if (items.event) items.event.apply(this, [value,form, that, config, ev]); // 事件 - if (items.added) items.added.apply(this, [ev,config,form]); + arry = [_that.event_rows_model.rows, _that.event_rows_model.index, ev, key, _that]; + break; + case 'sort': + var model = _that.config.column[data1.index]; + if ($(this).hasClass('sort-active')) $('.sort_' + _that.random + ' .sort-active').data({ + 'sort': 'desc' }); - } - }(items, key)); + $('.sort_' + _that.random).removeClass('sort-active').find('.glyphicon').removeClass('glyphicon-triangle-top').addClass('glyphicon-triangle-bottom'); + $(this).addClass('sort-active'); + if (data1.sort == 'asc') { + $(this).data({ + 'sort': 'desc' + }); + $(this).find('.glyphicon').removeClass('glyphicon-triangle-top').addClass('glyphicon-triangle-bottom'); + } else { + $(this).data({ + 'sort': 'asc' + }); + $(this).find('.glyphicon').removeClass('glyphicon-triangle-bottom').addClass('glyphicon-triangle-top'); + } + _that.config.sort = _that.config.sortParam({ + name: model.fid, + sort: data1.sort + }); + _that.$refresh_table_list(true); + break; + case 'checkbox': + var all = $(_that.config.el + ' [data-checkbox="all"]'), + checkbox_list = $(_that.config.el + ' tbody .checkbox_' + _that.random); + if (data1.checkbox == undefined) { + if (!$(this).hasClass('active')) { + $(this).addClass('active'); + _that.checkbox_list.push(index); + if (_that.data.length === _that.checkbox_list.length) { + all.addClass('active').removeClass('selected') + } else if (_that.checkbox_list.length > 0) { + all.addClass('selected'); + } + } else { + $(this).removeClass('active'); + _that.checkbox_list.splice(_that.checkbox_list.indexOf(index), 1); + if (_that.checkbox_list.length > 0) { + all.addClass('selected').removeClass('active'); + } else { + all.removeClass('selected active'); + } + } + } else { + if (_that.checkbox_list.length === _that.data.length) { + _that.checkbox_list = []; + checkbox_list.removeClass('active selected').next().prop('checked', 'checked') + all.removeClass('active'); + } else { + checkbox_list.each(function (index, item) { + if (!$(this).hasClass('active')) { + $(this).addClass('active').next().prop('checked', 'checked'); + _that.checkbox_list.push(index); + } + }); + all.removeClass('selected').addClass('active'); + } + } + _that.$set_batch_view(); + break; + case 'button': + arry.push(ev, _that); + break; + case 'search_focus': + var search_tips = $(_that.config.el + ' .bt_search_tips'); + if ($(_that.config.el + ' .bt_search_tips').length > 0) { + search_tips.remove(); + } + break; + case 'search_input': + if (ev.keyCode == 13) { + $(_that.config.el + ' .search_btn_' + _that.random).click(); + return false; + } + break; + case 'search_btn': + var _search = $(_that.config.el + ' .search_input'), val = $(_that.config.el + ' .search_input').val(); + val = val.replace(/\s+/g,""); + _search.val(val); + _that.config.search.value = val; + _search.append('
        ' + val + '
        '); + _that.$refresh_table_list(true); + break; + case 'page_select': + var limit = parseInt($(this).val()); + bt.set_cookie('page_number',limit); + _that.config.page.number = limit; + _that.config.page.page = 1; + _that.$refresh_table_list(true); + return false; + break; + case 'page_jump_input': + if (ev.keyCode === 13) { + $(_that.config.el + ' .page_jump_btn_' + _that.random).click(); + $(this).focus(); + } + return false; + break; + case 'page_jump_btn': + var jump_page = parseInt($(_that.config.el + ' .page_jump_input_' + _that.random).val()), + max_number = Math.ceil(_that.config.page.total / _that.config.page.number); + if(isNaN(jump_page)) jump_page = 1 + if (jump_page > max_number) jump_page = _that.config.page.page; + _that.config.page.page = jump_page; + _that.$refresh_table_list(true); + break; + case 'cut_page_number': + var page = parseInt($(this).attr('href').match(/([0-9]*)$/)[0]) + _that.config.page.page = page; + _that.$refresh_table_list(true); + return false; + break; + case 'eye_open_password': + if ($(this).hasClass('glyphicon-eye-open')) { + $(this).addClass('glyphicon-eye-close').removeClass('glyphicon-eye-open'); + $(this).prev().text(_that.data[index].password); + } else { + $(this).addClass('glyphicon-eye-open').removeClass('glyphicon-eye-close'); + $(this).prev().html('**********'); + } + return false; + break; + case 'copy_password': + bt.pub.copy_pass(_that.data[index].password); + return false; + break; + } + if (item.event) item.event.apply(this, arry); }); }); - if (callback) callback(); }, /** - * @description 获取表单数据 - * @return {object} 表单数据 - */ - $get_form_value:function(){ - return this.element.serializeObject(); - }, - - /** - * @description 设置指定数据 - * - */ - $set_find_value: function (name, value) { - var config = {},that = this; - typeof name != 'string' ? config = name : config[name] = value; - $.each(config, function (key, item) { - that.form_element[key].val(item); - }); - }, - - /** - * @description 获取Form,jquery节点 - * @param {Boolean} afresh 是否强制刷新 - * @return {object} - */ - $get_form_element: function (afresh) { - var form = {}, - that = this; - if (afresh || $.isEmptyObject(that.form_element)) { - this.element.find(':input').each(function (index) { - form[$(this).attr('name')] = $(this); - }); - that.form_element = form; - return form; - } else { - return that.form_element; - } - }, - - /** - * @description 验证值整个列表是否存在,存在则转换成属性字符串格式 - */ - $verify_group: function (config, group) { - var that = this, - str = ''; - $.each(group, function (index, item) { - if (typeof config[item] === "undefined") return true; - if (['disabled', 'readonly'].indexOf(item) > -1) { - str += ' ' + (config[item] ? (item + '="' + item + '"') : ''); - } else { - str += ' ' + item + '="' + config[item] + '"'; - } - }); - return str; - }, - - /** - * @description 验证绑定事件 - * @param {String} value - */ - $verify_bind_event: function (eventName, row, group) { - var event_list = {}; - $.each(group, function (index, items) { - var event_fun = row[items]; - if (event_fun) { - if (typeof event_list[eventName] === "object") { - if (!Array.isArray(event_list[eventName])) event_list[eventName] = [event_list[eventName]]; - event_list[eventName].push({ - event: event_fun, - eventType: items - }); + * @description 样式绑定 + * @param {array} style_list 样式列表 + * @return void + */ + $style_bind: function (style_list, status) { + var str = '', + _that = this; + $.each(style_list, function (index, item) { + if (item.css != '') { + if (!item.className) { + str += _that.config.el + ' thead th:nth-child(' + (item.index + 1) + '),' + _that.config.el + ' tbody tr td:nth-child' + (item.span ? ' span' : '') + '(' + (item.index + 1) + '){' + item.css + '}' } else { - event_list[eventName] = { - event: event_fun, - eventType: items - }; + str += item.className + '{' + item.css + '}'; } } }); - return event_list; + if ($('#bt_table_' + _that.random).length == 0) $(_that.config.el).append(''); }, /** - * @description 验证值是否存在 - * @param {String} value 内容/值 - * @param {String|Boolean} attr 属性 - * @param {String} type 属性 + * @deprecated 获取WIN高度或宽度 + * @return 返回当期的宽度和高度 */ - $verify: function (value, attr, type) { - if (!value) return ''; - if (type === true) return value ? ' ' + attr : ''; - if (type === 'style') return attr ? attr + ':' + value + ';' : value; - return attr ? ' ' + attr + '="' + value + '"' : ' ' + value; + $get_win_area: function () { + return [window.innerWidth, window.innerHeight]; }, + /** - * @description 验证form表单 + * @description 请求数据, + * @param {object} param 参数和请求路径 + * @return void */ - $verify_form:function(){ - var form_list = {},form = this.config.form,form_value = this.$get_form_value(),form_element = this.$get_form_element(true); - for(var key = 0; key< form.length; key++){ - var item = form[key]; - if(!Array.isArray(item.group)) item.group = [item.group]; - for(var i=0;i
        ' + }else{ + html += that.$reader_content_row(index, item); + } + }); + that.element = $('
        ' + html + '
        '); + _content = $('
        '); + _content.append(that.element); + if (callback) callback(); + return _content[0].outerHTML; + }, + + /** + * @description 渲染行内容 + * @param {object} data Form数据 + * @param {number} index 下标 + * @return {string} HTML结构 + */ + $reader_content_row: function (index, data) { + try { + var that = this,help = data.help || false,labelWidth = data.formLabelWidth || this.config.formLabelWidth; + if(data.display === false) return ''; + return '
        ' + + (typeof data.label !== "undefined"? '' + data.label + '' : '') + + '
        ' + + that.$reader_form_element(data.group, index) + + (help ? ('
        ' + help.list.join('
        ') + '
        ') : '') + + '
        ' + + '
        '; + } catch (error) { + console.log(error) + } + }, + + /** + * @description 渲染form类型 + * @param {object} data 表单数据 + * @param {number} index 下标 + * @return {string} HTML结构 + */ + $reader_form_element: function (data, index) { + var that = this,html = ''; + if (!Array.isArray(data)) data = [data]; + $.each(data, function (key, item) { + item.find_index = index; + html+= that.$reader_form_find(item); + that.form_config[item.name] = item; + }); + return html; + }, + /** + * @descripttion 渲染单个表单元素 + * @param {Object} item 配置 + * @return: viod + */ + $reader_form_find:function(item){ + var that = this,html = '',style = that.$reader_style(item.style) + _that.$verify(item.width, 'width', 'style'), + attribute = that.$verify_group(item, ['name', 'placeholder', 'disabled', 'readonly', 'autofocus', 'autocomplete', 'min', 'max']), + event_group = that.$create_event_config(item), + eventName = '',index = item.find_index; + if(item.display === false) return html + html += item.label ? '' + item.label + '' : ''; + if (typeof item['name'] !== "undefined"){ + that.$check_event_bind(item.name,event_group) + } + html += '
        '; + var _value = typeof that.data[item.name] !== "undefined" && that.data[item.name] != ''?that.data[item.name]:(item.value || '') + switch (item.type) { + case 'text': // 文本选择 + case 'checkbox': // 复选框 + case 'password': // 密码 + case 'radio': // 单选框 + case 'number': // 数字 + var _event = 'event_' + item.name + '_' + that.random; + switch(item.type){ + case 'checkbox': // 复选框 + html += ''; + that.$check_event_bind(_event + '_label',{'click':{type:'checkbox_icon',config:item}}) + that.$check_event_bind(_event,{'input':{type:'checkbox',config:item,event:item.event}}) + break; + default: + html += ''; + break; + } + if(item.btn && !item.disabled){ + html += ''+ item.btn.title +''; + if(typeof item.btn.event !== 'undefined'){ + that.$check_event_bind(item.name + '_btn', { + 'click': { + config: item, + event:item.btn.event + } + }) + } + } + if(item.icon){ + html += ''; + if(typeof item.icon.event !== 'undefined'){ + that.$check_event_bind(item.name + '_icon',{ + 'click': { + type: 'select_path', + select:item.icon.select || '', + config: item, + children:'.'+item.name + '_icon', + event:item.icon.event, + callback:item.icon.callback + } + }) + } + } + break; + case 'textarea': + html += ''; + $.each(['blur', 'focus', 'input'], function (index, items) { + if (item.tips) { + var added = null,event = {} + switch (items) { + case 'blur': + added = function (ev,item,element){ + if ($(this).val() === '') $(this).next().show(); + layer.close(item.tips.loadT); + $(ev.target).data('layer',''); + } + break; + case 'focus': + added = function (ev, item) { + $(this).next().hide(); + item.tips.loadT = layer.tips(tips, $(this), { + tips: [1, '#20a53a'], + time: 0, + area: $(this).width() + }); + } + break; + } + } + that.event_list[item.name][items] ? (that.event_list[item.name][items]['added'] = added) : (that.event_list[item.name][items] = { + type: item.type, + cust: false, + event: item[items], + added: added + }); + }); + if (item.tips) { + var tips = ''; + if (typeof item.tips.list === "undefined") { + tips = item.tips.text; + } else { + tips = item.tips.list.join('
        '); + } + html += '
        ' + tips + '
        '; + that.$check_event_bind(item.name + '_tips',{ + 'click': { + type: 'textarea_tips', + config: item + } + }) + } + break; + case 'select': + html += that.$reader_select(item, style, attribute, index); + that.$check_event_bind('custom_select',{ + 'click': { + type: 'custom_select', + children: '.bt_select_value' + } + }) + that.$check_event_bind('custom_select_item',{ + 'click': { + type: 'custom_select_item', + children: 'li.item' + } + }) + break; + case 'link': + eventName = 'event_link_' + that.random +'_'+ item.name; + html += '' + item.title + ''; + that.$check_event_bind(eventName,{ + 'click': { + type: 'link_event', + event: item.event + } + }) + break; + case 'button': + html += '' + break; + case 'help': + var _html = ''; + $.each(item.list, function (index, items) { + _html += '
      • ' + items + '
      • '; + }) + html += '
          ' + _html + '
        '; + break; + } + html += item.unit?''+ item.unit +'':''; + html += '
        ' + return html; + }, + + /** + * @descripttion 检测检测名称 + * @param {string} eventName 配置 + * @param {object} config 事件配置 + */ + $check_event_bind:function(eventName,config){ + if(!this.event_list[eventName]){ + // console.log(eventName) + if(!this.event_list.hasOwnProperty(eventName)){ + // console.log('1') + this.event_list[eventName] = config + } + } + }, + /** + * @description 创建事件配置 + * @param {object} item 行内配置 + * @return {object} 配置信息 + */ + $create_event_config: function (item) { + var config = {}; + if (typeof item['name'] === "undefined") return {}; + $.each(this.event_type, function (key, items) { + if (item[items]) { + config[(items === 'event' ? 'click' : items)] = { + type: item.type, + event: item[items], + cust: (['select', 'checkbox', 'radio'].indexOf(item.type) > -1), + config: item + }; + } + }); + return config; + }, + /** + * @description 渲染样式 + * @param {object|string} data 样式配置 + * @return {string} 样式 + */ + $reader_style: function (data) { + var style = ''; + if (typeof data === 'string') return data; + if (typeof data === 'undefined') return ''; + $.each(data, function (key, item) { + style += key + ':' + item + ';'; + }); + return style; + }, + /** + * @descripttion 局部刷新form表单元素 + * @param {String} name 需要刷新的元素 + * @param {String} name 元素新数据 + * @return: viod + */ + $local_refresh:function(name,config){ + var formFind = this.element.find('[data-name='+name+']') + if(this.element.find('[data-name='+name+']').length === 0) formFind = this.element.find('[name='+ name +']') + formFind.parent().replaceWith(this.$reader_form_find(config)) + }, + /** + * @description 渲染下拉,内容方法 + */ + $reader_select: function (item, style, attribute, index) { + var that = this,list = '',option = '',active = {}; + if(typeof item.list === 'function'){ + var event = item.list; + event.call(this,this.config.form); + item.list = []; + } + if (!Array.isArray(item.list)) { + var config = item.list; + bt_tools.send({ + url: config.url, + data: config.param || config.data || {} + }, function (res) { + if (res.status !== false) { + var list = item.list.dataFilter ? item.list.dataFilter(res, that) : res; + if (item.list.success) item.list.success(res, that, that.config.form[index], list) + item.list = list + if (!item.list.length) { + item.disabled = true + layer.msg(item.placeholder || '数据获取为空', {icon: 2}) + } + that.$replace_render_content(index); + } else { + bt.msg(res); + } + }); + return false + } + if(typeof that.data[item.name] === "undefined") active = item.list[0] + $.each(item.list, function (key, items) { + if(items.value === item.value || items.value === that.data[item.name]){ + active = items + return false + } + }) + $.each(item.list, function (key, items) { + list += '
      • ' + items.title + '
      • '; + option += ''; + }); + var title = !Array.isArray(item.list)?'Getting data...':(active?active.title:item.placeholder) + return '
        ' + + '' + (title || item.placeholder) + '' + + '
          ' + (list || '') + '
        ' + + '' + (option || '') + '' + + '
        '; + }, + + /** + * @description 替换渲染内容 + */ + $replace_render_content: function (index) { + var that = this,config = this.config.form[index],html = that.$reader_content_row(index, config); + $('[data-form=' + that.random + ']').find('.line:eq(' + index + ')').replaceWith(html); + this.$event_bind() + }, + + /** + * @description 重新渲染内容 + * @param {object} formConfig 配置 + */ + $again_render_form:function (formConfig){ + var formElement = $('[data-form=' + this.random + ']'),that = this; + formConfig = formConfig || this.config.form + formElement.empty() + for (var i = 0; i < formConfig.length; i++) { + var config = formConfig[i] + if(config.display === false) continue + formElement.append(that.$reader_content_row(i, config)) + } + this.config.form = formConfig + this.$event_bind() + }, + + /** + * @description 事件绑定功能 + * @param {Object} eventList 事件列表 + * @param {Function} callback 回调函数 + * @return void + */ + $event_bind: function (eventList, callback) { + var that = this,_event = {}; + that.element = $(typeof eventList === 'object' ? that.element : ('[data-form=' + that.random + ']')); + _event = eventList + if (typeof eventList === 'undefined') _event = that.event_list; + $.each(_event, function (key, item) { + if ($.isEmptyObject(item)) return true; + $.each(item, function (keys, items) { + if(!!item.type) return false; + if(!items.hasOwnProperty('bind')){ + items.bind = true + }else { + return false + } + var childNode = ''; + if (typeof items.cust === "boolean") { + childNode = '[' + (items.cust ? 'data-' : '') + 'name=' + key + ']'; + } else { + childNode = '.' + key; + } + (function (items, key) { + if (items.onEvent === false) { + switch (items.type) { + case 'input_checked': + $(childNode).on(keys != 'event' ? keys : 'click', function (ev) { + items.event.apply(this, [ev, that]); + }); + break; + } + return true; + } else { + if(items.type === 'select') return true + that.element.on(keys !== 'event' ? keys : 'click', items.children ? items.children : childNode, function (ev) { + var form = that.$get_form_element(true), config = that.form_config[key]; + switch (items.type) { + case 'textarea_tips': + $(this).hide().prev().focus(); + break; + case 'custom_select': + if ($(this).parent().hasClass('bt-disabled')) return false; + var select_value = $(this).next(); + if (!select_value.hasClass('show')) { + $('.bt_select_list').removeClass('show'); + select_value.addClass('show'); + } else { + select_value.removeClass('show'); + } + $(document).click(function () { + that.element.find('.bt_select_list').removeClass('show'); + $(this).unbind('click'); + return false; + }); + return false; + break; + case 'custom_select_item': + config = that.form_config[$(this).parents('.bt_select_updown').attr('data-name')] + var item_config = config.list[$(this).index()] + if ($(this).hasClass('disabled')) { + $(this).parent().removeClass('show'); + if (item_config.tips) layer.msg(item_config.tips, {icon: 2}); + return true; + } + if (!$(this).hasClass('active') && !$(this).hasClass('disabled')) { + var value = item_config.value.toString(); + $(this).parent().prev().find('.bt_select_content').text($(this).text()); + $(this).addClass('active').siblings().removeClass('active'); + $(this).parent().next().val(value) + $(this).parent().removeClass('show'); + } + that.data[config.name] = value + if(items.event) items.event = null + if(config.change) items.event = config.change + break; + case 'select_path': + bt.select_path('event_' + $(this).prev().attr('name') + '_' + that.random,items.select || "",!items.callback || items.callback.bind(that)); + break; + case 'checkbox': + var checked = $(this).is(':checked'); + if(checked){ + $(this).prev().addClass('active'); + }else{ + $(this).prev().removeClass('active'); + } + break; + } + if (items.event) items.event.apply(this, [that.$get_form_value(),form, that, config, ev]); // 事件 + if (items.added) items.added.apply(this, [ev,config,form]); + }); + } + }(items, key)); + }); + }); + if (callback) callback(); + }, + + /** + * @description 获取表单数据 + * @return {object} 表单数据 + */ + $get_form_value:function(){ + var form = {} + this.element.find('input,textarea[disabled="disabled"]').each(function (index,item){ + var val = $(this).val(); + if($(this).attr('type') === 'checkbox'){ val = $(this).prop('checked') } + form[$(this).attr('name')] = val + }) + return $.extend({},this.element.serializeObject(),form) + }, + + /** + * @description 设置指定数据 + * + */ + $set_find_value: function (name, value) { + var config = {},that = this; + typeof name != 'string' ? config = name : config[name] = value; + $.each(config, function (key, item) { + that.form_element[key].val(item); + }); + }, + + /** + * @description 获取Form,jquery节点 + * @param {Boolean} afresh 是否强制刷新 + * @return {object} + */ + $get_form_element: function (afresh) { + var form = {}, + that = this; + if (afresh || $.isEmptyObject(that.form_element)) { + this.element.find(':input').each(function (index) { + form[$(this).attr('name')] = $(this); + }); + that.form_element = form; + return form; + } else { + return that.form_element; + } + }, + + /** + * @description 验证值整个列表是否存在,存在则转换成属性字符串格式 + */ + $verify_group: function (config, group) { + var that = this, + str = ''; + $.each(group, function (index, item) { + if (typeof config[item] === "undefined") return true; + if (['disabled', 'readonly'].indexOf(item) > -1) { + str += ' ' + (config[item] ? (item + '="' + item + '"') : ''); + } else { + str += ' ' + item + '="' + config[item] + '"'; + } + }); + return str; + }, + + /** + * @description 验证绑定事件 + * @param {String} value + */ + $verify_bind_event: function (eventName, row, group) { + var event_list = {}; + $.each(group, function (index, items) { + var event_fun = row[items]; + if (event_fun) { + if (typeof event_list[eventName] === "object") { + if (!Array.isArray(event_list[eventName])) event_list[eventName] = [event_list[eventName]]; + event_list[eventName].push({ + event: event_fun, + eventType: items + }); + } else { + event_list[eventName] = { + event: event_fun, + eventType: items + }; + } + } + }); + return event_list; + }, + + /** + * @description 验证值是否存在 + * @param {String} value 内容/值 + * @param {String|Boolean} attr 属性 + * @param {String} type 属性 + */ + $verify: function (value, attr, type) { + if (!value) return ''; + if (type === true) return value ? ' ' + attr : ''; + if (type === 'style') return attr ? attr + ':' + value + ';' : value; + return attr ? ' ' + attr + '="' + value + '"' : ' ' + value; + }, + /** + * @description 验证form表单 + */ + $verify_form:function(){ + var form_list = {},form = this.config.form,form_value = this.$get_form_value(),form_element = this.$get_form_element(true); + for(var key = 0; key< form.length; key++){ + var item = form[key]; + if(!Array.isArray(item.group)) item.group = [item.group]; + if(item.separate) continue + for(var i=0;i'+ item.title +'
        '; - _tab_con += '
        '+ (active?item.content:'') +'
        '; + if (config.type && $.isEmptyObject(config.theme)) this.theme = this.theme_list[that.active]; + $.each(_list, function (index, item) { + var active = (that.active === index), + _active = _theme['active'] || 'active'; + _tab += '' + item.title + ''; + _tab_con += '
        ' + (active ? item.content : '') + '
        '; }); - that.element = $('
        '+ _tab +'
        '+ _tab_con +'
        '); + that.element = $('
        ' + _tab + '
        ' + _tab_con + '
        '); return that.element[0].outerHTML; - }, - /** - * @description 渲染指定tab内容 - */ - $reader_find:function(){ - }, /** * @description 事件绑定 + * */ - $event_bind:function(){ - var that = this,_theme = that.theme,active = _theme['active'] || 'active'; - if(!that.el) that.element = $('#tab_'+ that.random); - that.element.on('click',('.'+_theme['nav'].replace(/\s+/g,'.')+' span'),function(){ - var index = $(this).index(),config = that.config.list[index]; + $event_bind: function () { + var that = this,_theme = that.theme, + active = _theme['active'] || 'active'; + if (!that.el) that.element = $('#tab_' + that.random); + that.element.on('click', ('.' + _theme['nav'].replace(/\s+/g, '.') + ' span'), function () { + var index = $(this).index(), + config = that.config.list[index]; $(this).addClass(active).siblings().removeClass(active); - $('.'+_theme['body']+'>div:eq('+ index +')').addClass(active).siblings().removeClass(active); + $('#tab_' + that.random+' .' + _theme['body'] + '>div:eq(' + index + ')').addClass(active).siblings().removeClass(active); that.active = index; - if(!config.init){ - $('.'+_theme['body']+'>div:eq('+ index +')').html(config.content); - if(config.success) config.success(); + if (!config.init) { + // console.log(_theme) + $('#tab_' + that.random+' .' + _theme['body'] + '>div:eq(' + index + ')').html(config.content); + if (config.success) config.success(); config.init = true; } }); @@ -1791,12 +1878,19 @@ var bt_tools = { * @param {*} is_icon * @return void */ - load:function(title){ - var loadT = layer.msg(title +',please wait...',{icon:16,time:0,shade:.3}); - if(title === true) loadT = layer.load(); + load: function (title) { + var random = bt.get_random(5),layel = $('
        '+ title +',please wait...
        '),mask = '',loadT = ''; + $('body').append(layel); + var win = $(window),msak = $('.layer-loading-mask'),layel = $('#'+random); + layel.css({'top':((win.height() - 64)/2),'left':((win.width() -320)/2)}); + if (title === true) loadT = layer.load(); return { - close:function(){ - layer.close(loadT); + close: function () { + if(typeof loadT == "number"){ + layer.close(loadT); + }else{ + $('body').find('#' + random+',#layer-mask-'+ random).remove(); + } } } }, @@ -1805,1231 +1899,62 @@ var bt_tools = { * @param {object} config 和layer参数一致 * @require 当前关闭弹窗方法 */ - open:function(config){ - var _config = {},layerT = null,form = null; - _config = $.extend({type: 1,area: '640px',closeBtn: 2,btn: ['Yes', 'No']},config); - if(typeof _config.content == "object"){ - var param = _config.content; - form = bt_tools.form(param); - _config.success = function(layero,indexs){ - form.$event_bind(); - if(typeof _config.init != "undefined") _config.init(layero,indexs); - } - _config.yes = function(indexs,layero){ - var form_val = form.$verify_form(); - if(!form_val) return false; - if(typeof config.yes != "undefined"){ - var yes = config.yes.apply(form,[form_val,indexs,layero]); - if(!yes) return false; - } - } - _config.content = form.$reader_content(); + open: function (config) { + var _config = {},layerT = null,form = null; + _config = $.extend({type: 1,area: '640px',closeBtn: 2,btn: ['Yes', 'No']},config); + if(typeof _config.content == "object"){ + var param = _config.content; + form = bt_tools.form(param); + _config.success = function(layero,indexs){ + form.$event_bind(); + if(typeof config.success != "undefined") config.success(layero,indexs); } - layerT = layer.open(_config); - return { - close: function () { - layer.close(layerT); - }, - form:form + _config.yes = function(indexs,layero){ + var form_val = form.$verify_form(); + if(!form_val) return false; + if(typeof config.yes != "undefined"){ + var yes = config.yes.apply(form,[form_val,indexs,layero]); + if(!yes) return false; + } } + _config.content = form.$reader_content(); + } + layerT = layer.open(_config); + return { + close: function () { + layer.close(layerT); + }, + form:form + } }, /** * @description 封装msg方法 * @param {object|string} param1 配置参数,请求方法参数 * @param {number} param2 图标ID - * @require 当前关闭弹窗方法 + * @require 当前关闭弹窗方法 */ - msg:function(param1,param2){ - var layerT = null,msg = '',config = {}; - if(typeof param1 === "object"){ - if(typeof param1.status === "boolean"){ - msg = param1.msg,config = {icon:param1.status?1:2}; + msg: function (param1, param2) { + var layerT = null, + msg = '', + config = {}; + if (typeof param1 === "object") { + if (typeof param1.status === "boolean") { + msg = param1.msg, config = {icon: param1.status ? 1 : 2}; + if(!param1.status) config = $.extend(config,{time:(!param2?0:3000),closeBtn:2,shade:.3}); } } - if(typeof param1 === "string"){ - msg = param1,config = {icon:typeof param2 !== 'undefined'?param2:1} + if (typeof param1 === "string") { + msg = param1, config = { + icon: typeof param2 !== 'undefined' ? param2 : 1 + } } - layerT = layer.msg(msg,config); + layerT = layer.msg(msg, config); return { - close:function(){ + close: function () { layer.close(layerT); } } }, - /** - * @description 验证值是否存在 - * @param {String} value 内容/值 - * @param {String|Boolean} attr 属性 - * @param {String} type 属性 - */ - $verify:function(value,attr,type){ - if(!value) return ''; - if(type === true) return value?' '+ attr:''; - if(type === 'style') return attr?attr+':'+value+';':value; - return attr?' '+ attr+'="'+value+'"':' '+value; - }, - - /** - * @description 选择目录 - * @param {*} datas - * @param {*} callback - * @param {*} type - * @param {*} limit - */ - select_path:function(datas,callback,type,limit){ - //if(typeof data === 'string') data = {path:data}; - //if(typeof data === 'function') type = callback,callback = data,data = {path:bt.get_cookie('Path')}; - if(typeof type !== 'number') type = 1; - if(typeof limit !== 'number') limit = false; - bt.set_cookie('Path','/www/server/panel/BTPanel/static'); - var that = this, - select_file = { - type:type, - type_tips:type == 0?'目录':(type == 1?'文件':'目录或文件'), - type_limit:type == 0?'dir':(type == 1?'file':'all'), - select_list:[], - select_path:bt.get_cookie('Path') || (bt.os == 'Windows'?setup_path:'/www/wwwroot'), - select_config:{}, - scroll_width:0, - file_path: bt.get_cookie('Path'), - file_operating:[], - area : [window.innerWidth, window.innerHeight], - reader_view:function(){ - var _this = this; - this.scroll_width = select_file.getScrollbarWidth(); - layer.open({ - type:1, - title: datas.title || ('选择' + _this.type_tips), - shadeClose:false, - closeBtn: 2, - area:['650px','555px'], - content:'
        \ -
        \ -
        \ -
        \ - \ - \ - \ -
        \ -
        \ -
        \ -
        \ - \ -
        \ -
        \ -
        \ - \ - \ - \ -
        \ -
        \ -
        \ -
        \ -
        \ -
        \ -
        \ - \ - \ -
        名称修改时间类型
        \ -
        \ -
        \ -
        \ - \ - \ - \ -
        文件修改时间类型
        \ -
        \ -
        \ -
        \ -
        \ -
        \ - '+ _this.type_tips +'名:\ -
        \ - \ -
        \ -
        \ -
        \ - \ - \ -
        \ -
        \ -
        ', - success:function(){ - //_this.event_bind(); - _this.render_file_list(); - $('.select_list_thaed').css('right',_this.scroll_width+'px'); - } - }); - }, - /** - * @description 渲染文件列表 - * @param {object} data 配置参数,包含path路径、serarch搜索字段 - */ - render_file_list:function(data,callback){ - var mount_html = '',dir_html = '',_this = this,datas = {path:this.select_path,sort:'type',disk:true}; - if(typeof data == "undefined") data = datas; - if(typeof data == "function") callback = data,data = datas; - _this.select_config = $.extend(datas,data); - that.$http('GetDir',_this.select_config,function(rdata){ - var disk_list = rdata.DISK,dir_list = $.merge(_this.data_reconstruction(rdata.DIR,'DIR') ,_this.data_reconstruction(rdata.FILES)); - _this.select_list = dir_list; - _this.render_path_list(); - $.each(disk_list,function(index,item){ - var name = (item.path == '/'?'根目录':item.path.indexOf(':/') > -1?('本地磁盘('+ item.path.match(/[A-Z]+/)[0])+ ':)':item.path) - mount_html += '
        '+ - ''+ - ''+ name +''+ - '
        '; - }); - - $.each(dir_list,function(index,item){ - if((_this.type == 0) && item.type != _this.type_limit) return true; - var mtime = bt.format_data(item.mtime), - type = (item.type == 'dir'?'文件夹':'文件'); - dir_html += '\ - '+ item.filename +'\ - '+ mtime +'\ - '+ (item.type == 'dir'?'文件夹':'文件') +'\ - '; - }); - $('#select_mount_list').html(mount_html); - $('#select_dir_list').html(dir_html); - if(callback) callback(rdata); - }); - }, - /** - * @description 渲染路径列表 - * @param {Function} callback 回调函数 - * @return void - */ - - data_reconstruction:function(data,type,callback){ - if(data.length < 1) return []; - var _array = []; - $.each(data,function(index,item){ - var itemD = item.split(";"),fileMsg ='',fileN = itemD[0].split('.'),extName = fileN[fileN.length - 1]; - switch(itemD[0]) { - case '.user.ini': - fileMsg = 'PS: PHP用户配置文件(防跨站)!'; - break; - case '.htaccess': - fileMsg = 'PS: Apache用户配置文件(伪静态)'; - break; - case 'swap': - fileMsg = 'PS: 宝塔默认设置的SWAP交换分区文件'; - break; - } - if(itemD[0].indexOf('Recycle_bin') != -1) fileMsg = 'PS: 回收站目录,勿动!'; - if(itemD[0].indexOf('.upload.tmp') != -1) fileMsg = 'PS: 宝塔文件上传临时文件,重新上传从断点续传,可删除'; - _array.push({ - caret: itemD[8] == '1'?true:false, //是否收藏 - down_id: itemD[9], //是否分享 分享id - ext: (type == 'DIR'?'':extName), //文件类型 - filename: itemD[0], //文件名称 - mtime: itemD[2], //时间 - ps: fileMsg || itemD[10], //备注 - size: itemD[1], //文件大小 - type: type == 'DIR'?'dir':'file', //文件类型 - user: itemD[3], //用户权限 - root_level:itemD[4] //所有者 - // accept: 666, - //composer: 0, - // link: "", - // shell: false, - }) - - }) - return _array; - }, - render_path_list: function (callback){ - var _this = this,html = '
        ', path_before = '',dir_list = this.file_path.split("/").splice(1),first_dir = this.file_path.split("/")[0]; - if(bt.os === 'Windows'){ - if(dir_list.length == 0) dir_list = []; - dir_list.unshift('本地磁盘('+ first_dir +')'); - }else{ - if(this.file_path == '/') dir_list = []; - dir_list.unshift('根目录'); - } - for(var i = 0; i < dir_list.length; i++){ - path_before += '/' + dir_list[i]; - if (i == 0) path_before = ''; - html += '
        \ - ' + dir_list[i] + '\ - \ - \ -
        '; - } - $('.path_input').val('').attr('data-path',this.file_path); - var file_dir_view = $('.file_path_input .file_dir_view'); - file_dir_view.html(html); - file_dir_view.attr('data-width',file_dir_view.width()); - _this.set_dir_view_resize(); - }, - - /** - * @description 设置标题宽度 - * @param {string} th_index 表头序列号 - * @param {string} style class样式 - */ - set_select_width:function(th_index,style){ - var _this = this,className = '.select_dir_list tbody td:nth-child('+ (th_index+1) +')>span,.select_dir_list thead th:nth-child('+ (th_index+1) +')>span',style_index = $('#th-index-'+th_index); - if(style_index.length > 0){ - style_index.html(className +'{'+ style +'}'); - }else{ - $('#select_file_style').append(''); - } - }, - /** - * @description 样式转换文件样式 - * @param {string} style 样式 - */ - cut_style_object:function(style){ - var object = {},arry = style.split(';'); - console.log(arry); - return object; - }, - /** - * @description 获取滚动条宽度 - */ - getScrollbarWidth:function(){ - var odiv = document.createElement('div'),//创建一个div - styles = { - width: '100px', - height: '100px', - overflowY: 'scroll'//让他有滚动条 - }, i, scrollbarWidth; - for (i in styles) odiv.style[i] = styles[i]; - document.body.appendChild(odiv);//把div添加到body中 - scrollbarWidth = odiv.offsetWidth - odiv.clientWidth;//相减 - odiv.remove();//移除创建的div - return scrollbarWidth;//返回滚动条宽度 - }, - set_dir_view_resize:function(){ - var file_path_input = $('.file_path_input'),file_dir_view = $('.file_path_input .file_dir_view'),_path_width = file_dir_view.attr('data-width'),file_item_hide = null; - if(_path_width){ - parseInt(_path_width); - }else{ - _path_width = file_dir_view.width(); - file_dir_view.attr('data-width',_path_width); - } - if(file_dir_view.width() - _path_width < 90){ - var width = 0; - $($('.file_path_input .file_dir_view .file_dir_item').toArray().reverse()).each(function(){ - var item_width = 0; - if(!$(this).attr('data-width')){ - $(this).attr('data-width',$(this).width()); - item_width = $(this).width(); - }else{ - item_width = parseInt($(this).attr('data-width')); - } - width += item_width; - if((file_path_input.width() - width) <= 90){ - $(this).addClass('hide'); - }else{ - $(this).removeClass('hide'); - } - }); - } - var file_item_hide = file_dir_view.children('.file_dir_item.hide').clone(true); - if(file_dir_view.children('.file_dir_item.hide').length == 0){ - file_path_input.removeClass('active').find('.file_dir_omit').addClass('hide'); - }else{ - file_item_hide.each(function(){ - if($(this).find('.glyphicon-hdd').length == 0){ - $(this).find('.file_dir').before(''); - } - }); - file_path_input.addClass('active').find('.file_dir_omit').removeClass('hide'); - file_path_input.find('.file_dir_omit .nav_down_list').empty().append(file_item_hide); - file_path_input.find('.file_dir_omit .nav_down_list .file_dir_item').removeClass('hide'); - } - }, - /** - * @description 渲染文件列表 - * @param {Object} data 参数对象,例如分页、显示数量、排序,不传参数使用默认或继承参数 - * @param {Function} callback 回调函数 - * @return void - */ - reader_file_list:function (data, callback){ - var _this = this,select_page_num = '',next_path = ''; - if(typeof data === "function") callback = data,data = {is_operating:false}; - if(typeof data === "undefined") data = {is_operating:false}; - if(limit){ - layer.msg('只能在该目录下操作',{icon:2}); - return false; - } - this.loadT = bt.load('正在获取文件列表,请稍候...'); - this.file_images_list = []; - _this.get_dir_list(data,function(res){ - _this.loadT.close(); - _this.file_list = $.merge(_this.data_reconstruction(res.DIR,'DIR'), _this.data_reconstruction(res.FILES)); - _this.file_path = res.PATH; - _this.is_recycle = res.FILE_RECYCLE; - _this.file_store_list = res.STORE; - bt.set_cookie('Path',res.PATH); - - - var mount_html = '',dir_html = '', - disk_list = res.DISK, - dir_list = _this.file_list; - _this.select_list = _this.file_list; - _this.render_path_list(); - $.each(disk_list,function(index,item){ - var name = (item.path == '/'?'根目录':item.path.indexOf(':/') > -1?('本地磁盘('+ item.path.match(/[A-Z]+/)[0])+ ':)':item.path) - mount_html += '
        '+ - ''+ - ''+ name +''+ - '
        '; - }); - - $.each(dir_list,function(index,item){ - if((_this.type == 0) && item.type != _this.type_limit) return true; - var mtime = bt.format_data(item.mtime), - type = (item.type == 'dir'?'文件夹':'文件'); - dir_html += '\ - '+ item.filename +'\ - '+ mtime +'\ - '+ (item.type == 'dir'?'文件夹':'文件') +'\ - '; - }); - $('#select_mount_list').html(mount_html); - $('#select_dir_list').html(dir_html); - var _i = res.PATH.lastIndexOf("\/"), - index_file = res.PATH.substring(_i + 1, res.PATH.length); - //if(_this.type_limit != 'file') $('[name="select_name"].file_directory').val(index_file); - if(callback) callback(rdata); - }); - }, - - - /** - * @description 渲染文件列表内容 - * @param {Object} data 文件列表数据 - * @param {Function} callback 回调函数 - * @return void - */ - reader_file_list_content:function(data,callback){ - var _html = '',_this = this,is_dir_num = 0; - $.each(data,function(index,item){ - var _title = item.filename + item.ps; - _this.file_list[index] = item = _this.$file_data_dispose(item); - _this.file_list[index]['only_index'] = index; - if(item.filename.indexOf('Recycle_bin')) _tips = 'PS: 回收站目录,勿动!'; - if(_title.length > 20) _title = _title.substring(0, 20) + '...'; - if(bt.check_chinese(_title) && _title.length > 10) _title = _title.substring(0, 10) + '...' - _html += '
        '+ - '
        '+ - '
        '+ - '
        '+ - ''+ item.filename + item.ps +''+ (item.caret?'':'') + (item.down_id?'':'') + - '
        '+ - '
        '+ item.type_tips +'
        '+ - '
        '+ (item.type == 'dir'?'点击计算':bt.format_size(item.size)) +'
        '+ - '
        ' + bt.format_data(item.mtime) + '
        '+ - '
        '; - if(item.type == 'dir') is_dir_num ++; // 获取目录数量 - if(item.ispreview){ // 获取图片数量 - _this.file_images_list.push(item.path); - if(typeof item.images_id) item.images_id = _this.file_images_list.length - 1; - } - - }); - $('.file_list_content').html(_html); - if(callback) callback({is_dir_num:is_dir_num}) - }, - /** - * @description 文件数据处理 - * @param {Object} data 配置参数 - * @return {Object} 重新生成的文件对象 - */ - $file_data_dispose:function(data){ - var models = {languages:null,model:null}; - //if(data.type !== 'dir') models = this.$get_file_model(data); - return $.extend(data,{ - only_id:bt.get_random(10), // 文件唯一ID - type_tips:data.type === 'dir'?'文件夹':this.ext_type_tips(data.ext), // 文件类型描述 - open_type:this.determine_file_type(data.ext), // 打开类型 - languages:models.languages, // 文件语言 - path:data.path || this.path_resolve(this.file_path,data.filename), // 文件全路径 - model:models.model, // 语言模型 - }); - }, - /** - * @description 文件路径合并 - * @param {String} paths 旧路径 - * @param {String} param 新路径 - * @return {String} 新的路径 - */ - path_resolve:function(paths, param){ - var path = '',split = ''; - if(!Array.isArray(param)) param = [param]; - paths.replace(/([\/|\/]*)$/,function($1){ - split = $1; - return 'www'; - }); - $.each(param,function(index,item){ - path += '/' + item; - }); - return paths + path; - }, - /** - * @description 获取文件语言配置模型 - * @param {Object} data 当前文件的数据对象 - * @return {Object} 当前语言配置模型 - */ - $get_file_model:function(data){ - var config = this.vscode_editor.config; - for(languages in config.supportedModes){ - var item = config.supportedModes[languages]; - for(var j=0;j'+ - ''+ - ''+(item.path == '/'?'根目录':item.path)+' ('+ item.size[2] +')'+ - ''; - }); - $('.mount_disk_list').html(html); - }); - }, - /** - * @description 文件类型判断,或返回格式类型(不传入type) - * @param {String} ext - * @param {String} type - * @return {Boolean|Object} 返回类型或类型是否支持 - */ - determine_file_type:function(ext,type){ - var config = { - images:['jpg','jpeg','png','bmp','gif','tiff','ico'], - compress:['zip','rar','gz','war','tgz'], - video:['mp4', 'mpeg', 'mpg', 'mov', 'avi', 'webm', 'mkv'], - ont_text:['iso','xlsx','xsl','doc','docx','tiff','exe','so','7z','bz','dmg','apk'] - },returnVal = false; - if(type != undefined){ - if(type == 'text'){ - $.each(config,function(key,item){ - $.each(item,function(index,items){ - if(items == ext){ - returnVal = true; - return false; - } - }) - }); - returnVal = !returnVal - }else{ - if(typeof config[type] == "undefined") return false; - $.each(config[type],function(key,item){ - if(item == ext){ - returnVal = true; - return false; - } - }); - } - }else{ - $.each(config,function(key,item){ - $.each(item,function(index,items){ - if(items == ext){ - returnVal = key; - return false; - } - }) - }); - if(typeof returnVal == "boolean") returnVal = 'text'; - } - return returnVal; - }, - - /** - * @description 渲染右键鼠标菜单 - * @param {Object} ev 事件event对象 - * @param {Object} el 事件对象DOM - * @return void - */ - render_file_groud_menu: function (ev,el){ - var _this = this,index = $(el).data('index'),data = _this.file_list[index],config_group = [['open','打开'],['split',''],['download','下载'],['share','分享目录/文件'],['cancel_share','取消分享'],['favorites','收藏目录/文件'],['cancel_favorites','取消收藏'],['split',''],['dir_kill','目录查杀'],['authority','权限'],['split',''],['copy','复制'],['shear','剪切'],['rename','重命名'],['del','删除'],['split',''],['killing','创建压缩',[['gzip','tar.gz (推荐)'],['zip','zip (通用格式)'],['rar','rar (中文兼容较好)']]],['unzip','解压',[['local','解压到当前'],['folad','解压到当前']]]],compression = ['zip','rar','gz','war','tgz','bz2'],offsetNum = 0; - if(data.type == 'dir'){ // 判断是否为目录,目录不可下载 - config_group.splice(2,1); - offsetNum ++; - } - if(data.down_id !== 0){ //判断是否分享 - config_group.splice(3-offsetNum,1); - offsetNum ++; - }else{ - config_group.splice(4-offsetNum,1); - config_group[3-offsetNum][1] = (data.type == 'dir'?'分享目录':'分享文件'); - offsetNum ++; - } - if(data.caret !== false){ // 判断是否收藏 - config_group.splice((5-offsetNum),1); - offsetNum ++; - }else{ - config_group.splice((6-offsetNum),1); - config_group[5-offsetNum][1] = (data.type == 'dir'?'收藏目录':'收藏文件'); - offsetNum ++; - } - if(data.ext != 'php' && data.type != 'dir'){ // 判断是否为php,非php文件(排除目录)无法扫描 - config_group.splice((8-offsetNum),1); - offsetNum ++; - } - var num = 0; - $.each(compression,function(index,item){ - if(item == data.ext) num ++; - }); - if(num == 0){ - config_group.splice((17-offsetNum),1); - offsetNum ++; - } - _this.reader_menu_list({el:$('.selection_right_menu'),ev:ev,data:data,list:config_group}); - }, - - /** - * @description 渲染右键全局菜单 - * @param {Object} ev 事件event对象 - * @param {Object} el 事件对象DOM - * @return void - */ - render_file_all_menu:function (ev,el) { - var _this = this,config_group = [['refresh','刷新'],['split',''],['upload','上传'],['create','新建文件夹/文件',[['create_dir','新建文件夹'],['create_files','新建文件']]],['split',''],['paste','粘贴']],offsetNum = 0; - if(!bt.get_storage('session','copy_path')){ - config_group.splice(5,1); - offsetNum ++; - } - _this.reader_menu_list({el:$('.selection_right_menu'),ev:ev,data:{},list:config_group}); - }, - get_dir_list:function(data, callback,is_tips) { - var _this = this; - if(typeof callback === "boolean") is_tips = callback,callback = null; - that.$http('GetDir',$.extend({ - disk:true, - path:_this.file_path, - sort:bt.get_cookie('files_sort') || 'type', - },data),callback,is_tips); - }, - /** - * @description 返回上一层目录地址 - * @param {String} path 当前路径 - * @returns 返回上一层地址 - */ - retrun_prev_path:function(path){ - var dir_list = path.split('/'); - dir_list.splice(dir_list.length - 1); - return dir_list.join('/'); - }, - - /** - * @description 渲染菜单列表 - * @param {Object} config 菜单配置列表和数据 - * @returns void - */ - reader_menu_list:function(config){ - var _this = this,el = config.el.find('ul'),el_height = el.height(),el_width = el.width(),left = config.ev.clientX - ((this.area[0] - config.ev.clientX) < el_width?el_width:0); - el.empty(); - $.each(config.list,function(index,item){ - var $children = null,$children_list = null; - if(item[0] == 'split'){ - el.append('
      • '); - }else{ - if(Array.isArray(item[2])){ - $children = $('
          '); - $children_list = $children.find('.set_group'); - $.each(item[2],function(indexs,items) { - $children_list.append($('
        • '+ items[1] +'
        • ').on('click',{type:items[0],data:config.data},function(ev){ - _this.file_groud_event($.extend(ev.data.data,{ - open:ev.data.type, - index:parseInt($(config.ev.currentTarget).data('index')), - element:config.ev.currentTarget, - type_tips:item.type?'文件夹':'文件' - })); - config.el.removeAttr('style'); - ev.stopPropagation(); - ev.preventDefault(); - })) - }); - } - el.append($('
        • '+ item[1] +'
        • ').append($children).on('click',{type:item[0],data:config.data},function(ev){ - _this.file_groud_event($.extend(ev.data.data,{ - open:ev.data.type, - index:parseInt($(config.ev.currentTarget).data('index')), - element:config.ev.currentTarget, - type_tips:item.type?'文件夹':'文件' - })); - config.el.removeAttr('style'); - ev.stopPropagation(); - ev.preventDefault(); - })); - } - }); - config.el.css({ - left: left, - top: config.ev.clientY - ((this.area[1] - config.ev.clientY) < el_height?el_height:0) - }).removeClass('left_menu right_menu').addClass(this.area[0] - (left + el_width) < 230?'left_menu':'right_menu'); - $(document).one('click',function(e){ - $(config.ev.currentTarget).removeClass('selected'); - config.el.removeAttr('style'); - e.stopPropagation(); - e.preventDefault(); - }); - }, - - - /** - * @description 返回后缀类型说明 - * @param {String} ext 后缀类型 - * @return {String} 文件类型 - */ - ext_type_tips:function(ext){ - var config = {ai:"Adobe Illustrator格式图形",apk:"安卓安装包",asp:"动态网页文件",bat:"批处理文件",bin:"二进制文件",bas:"BASIC源文件",bak:"备份文件",css:'CSS样式表',cad:"备份文件",cxx:"C++源代码文件",crt:"认证文件",cpp:"C++代码文件",conf:"配置文件",dat:"数据文件",der:"认证文件",doc:"Microsoft Office Word 97-2003 文档",docx:"Microsoft Office Word 2007 文档",exe:"程序应用",gif:"图形文件",go:"Go语言源文件",htm:"超文本文档",html:"超文本文档",ico:"图形文件",java:"Java源文件",jsp:"HTML网页",jpe:"图形文件",jpeg:"图形文件",jpg:"图形文件",log:"日志文件",link:"快捷方式文件",js:"Javascript源文件",mdb:"Microsoft Access数据库",mp3:"音频文件",mp4:"视频文件",mng:"多映像网络图形",msi:"Windows Installe安装文件包",png:"图形文件",py:"Python源代码",pyc:"Python字节码文件",pdf:"文档格式文件",ppt:"Microsoft Powerpoint 97-2003 幻灯片演示文稿",pptx:"Microsoft Powerpoint2007 幻灯片演示文稿",psd:"Adobe photoshop位图文件",pl:"Perl脚本语言",rar:"RAR压缩文件",reg:"注册表文件",sys:"系统文件",sql:"数据库文件",sh:"Shell脚本文件",txt:"文本格式",vb:"Visual Basic的一种宏语言",xml:"扩展标记语言",xls:"Microsoft Office Excel 97-2003 工作表",xlsx:"Microsoft Office Excel 2007 工作表",gz:"压缩文件",zip:"ZIP压缩文件",z:"","7z":"7Z压缩文件",json:'JSON文本'}; - return typeof config[ext] != "undefined"?config[ext]:(ext+'文件'); - }, - /** - * @description 事件绑定 - */ - event_bind: function(){ - var _this = this; - //单击选中文件 - $('#select_dir_list').on('click','tr[data-index]',function(e){ - var index = $(this).data('index'), - _type = $(this).attr('data-type'), - select_name_val = (_this.type_limit=='file' && _type == 'dir')?'':_this.select_list[index].filename; - $(this).addClass('active').siblings().removeClass('active'); - $('[name="select_name"]').val(select_name_val); - if (_type == 'file'){ - $('.file_path_views .forward_path span:eq(1)').addClass('active'); - }else{ - $('.file_path_views .forward_path span:eq(1)').removeClass('active'); - } - }); - $('.select_list_body').on('scroll',function(e){ - var top = $(this).scrollTop(),left = $(this).scrollLeft(); - if(top > 0){ - $('.select_list_shadow').show(); - }else{ - $('.select_list_shadow').hide(); - } - if(left >= 0){ - $('.select_list_thaed').css('left',(4-left ) +'px'); - } - e.stopPropagation(); - e.preventDefault(); - }); - //文件刷新按钮 - $('.file_path_refresh').on('click',function(){ - _this.render_file_list(); - }); - $('.select_dir_list thead th .icon-drag').on('mousedown',function(ev){ - var x = ev.clientX,th = $(this).parent(),th_index = th.index(),th_width = th[0].clientWidth,min_width = parseInt($(this).parent().attr('data-min')),timeOut = null; - if(ev.which === 1){ - var document_mousemove = function(e){ - var move_x = e.clientX,offset_x = move_x - x,_width = th_width + offset_x; - timeOut = setTimeout(function(){ - _this.set_select_width(th_index,'width:'+_width+'px'); - },0); - }, - document_mouseup = function(){ - console.log(document_mouseup); - $(this).unbind(document_mousemove); - $(this).unbind(document_mouseup); - }; - $(document).on('mousemove',document_mousemove).one('mouseup',function(){ - $(this).unbind(document_mousemove); - $(this).unbind(this); - }) - } - ev.stopPropagation(); - }); - $('.select_dir_list thead th>span').on('click',function(ev){ - var th = $(this).parent(),type = th.data('type'),is_active = th.hasClass('active'),is_sort = th.hasClass('sort'); - if(is_active){ - th.addClass('sort').removeClass('active'); - }else if(is_sort){ - th.addClass('active').removeClass('sort'); - }else{ - th.addClass('active'); - } - th.siblings().removeClass('active sort'); - _this.render_file_list({sort:type,reverse:is_active?1:0}); - ev.stopPropagation(); - }); - $('.search_path_views input').on('keyup',function(){ - var val = $(this).val(); - console.log(val); - }); - $('.search_path_views input').focus(function(){ - $(this).next().show(); - }).blur(function(){ - $(this).next().hide(); - }); - //提交选中文件 - $('#select_file_directory .select_file_group').on('click','.select_btn_group .btn-success',function(e){ - var select_val = $('[name="select_name"]').val(), - submit_val = ''; - if (select_val == '') { - layer.msg('只能选择' + _this.type_tips,{icon:2}); - }else{ - submit_val = _this.file_path+'/'+select_val; - console.log(submit_val) - } - }); - //新建文件夹 - $('#select_file_directory .select_file_tootls').on('click','.creat_dir',function(e){ - var a = "   "; - if($("#tbody tr").length == 0) { - $("#select_dir_list").append(a) - } else { - $("#select_dir_list tr:first-child").before(a) - } - $(".newFolderName").focus(); - $("#nameOk").click(function() { - var c = $("#newFolderName").val(), - b = $(".file_path_views .path_input").attr("data-path"); - newTxt = b+"/"+ c; - that.$http('CreateDir', {'path': newTxt},function(e){ - if(e.status == true) { - _this.reader_file_list({path:b,is_operating:false}); - layer.msg(e.msg, {icon: 1}); - } else { - layer.msg(e.msg, {icon: 2}); - } - }); - }); - }); - // 左侧根目录点击跳转 - $('#select_mount_list').on('click','.item',function(){ - _this.reader_file_list({path:$(this).attr('data-menu'),is_operating:true}); - }); - - - // 窗口大小限制 - $(window).resize(function(ev){ - if($(this)[0].innerHeight != _this.area[1]){ - _this.area[1] = $(this)[0].innerHeight; - _this.set_file_view(); - } - if($(this)[0].innerWidth != _this.area[0]){ - _this.area[0] = $(this)[0].innerWidth; - _this.set_dir_view_resize(); - // _this.set_file_table_width(); - } - if(_this.vscode_editor.view){ - if(_this.vscode_editor.is_full_min > 0){ - layer.style(_this.vscode_editor.view,{ - 'top':0, - 'left':0,'width':_this.area[0], - 'height':_this.area[1] - }); - } - } - if(_this.vscode_editor.view && $.isEmptyObject(_this.vscode_editor.list)) _this.vscode.layout(); - // console.log($.isEmptyObject(_this.vscode_editor.list)); - }).keydown(function(e){ // 全局按键事件 - e = window.event || e; - var keyCode = e.keyCode,tagName = e.target.tagName.toLowerCase(); - if(keyCode == 8 && tagName !== 'input' && tagName !== 'textarea'){ //退格键 -> 后退操作 - if(_this.vscode_editor.view == null){ - $('.forward_path span:eq(0)').click(); - } - return false; - } - }); - - // 文件路径事件(获取焦点、失去焦点、回车提交) - $('.file_path_input .path_input').on('focus blur keyup',function(e){ - e = e || window.event; - var path = $(this).attr('data-path'); - switch(e.type){ - case 'focus': - $(this).addClass('focus').val(path).prev().hide(); - break; - case 'blur': - $(this).removeClass('focus').val('').prev().show(); - break; - case 'keyup': - if(e.keyCode != 13 && e.type == 'keyup') return false; - var _val = $(this); - if($(this).data('path') != $(this).val()){ - _this.reader_file_list({path:$(this).val(),is_operating:true},function(res){ - if(res.status === false){ - $(_val).val(path); - }else{ - $(_val).val(res.PATH); - $(_val).blur().prev().show(); - } - }); - } - break; - } - e.stopPropagation(); - }); - - // 文件路径点击跳转 - $('.file_path_input .file_dir_view').on('click','.file_dir',function(){ - _this.reader_file_list({path:$(this).attr('title'),is_operating:true}); - }); - - // 操作前进或后退 - $('.forward_path span').click(function(){ - var index = $(this).index(),path = ''; - if(!$(this).hasClass('active')){ - switch(index){ - case 0: - _this.file_pointer = _this.file_pointer - 1 - path = _this.retrun_prev_path(_this.file_path); - break; - case 1: - _this.file_pointer = _this.file_pointer + 1 - //path = _this.file_operating[_this.file_pointer]; - path = _this.file_path + '/' + $(".select_file_group .file_directory").val(); - break; - case 2: - _this.file_pointer = _this.file_pointer - 1 - path = _this.retrun_prev_path(_this.file_path); - break; - } - _this.reader_file_list({path:path,is_operating:false}); - } - }); - - //展示已隐藏的目录 - $('.file_path_input .file_dir_view').on('click','.file_dir_omit',function(e){ - var _this = this,new_down_list = $(this).children('.nav_down_list'); - $(this).addClass('active'); - new_down_list.addClass('show'); - $(document).one('click',function(){ - $(_this).removeClass('active'); - new_down_list.removeClass('show'); - e.stopPropagation(); - }); - e.stopPropagation(); - }); - - // 打开文件夹和文件 --- 双击 - $('#select_dir_list').on('dblclick','tr',function(e){ - if($(e.target).hasClass('file_check')) return false; - var path = $(this).attr('data-path'),type = $(this).attr('data-type'); - if(type == 'dir'){ - _this.reader_file_list({path:_this.file_path + '/' + path}); - }else{ - - } - e.stopPropagation(); - e.preventDefault(); - }); - - // 文件刷新 - $('.file_path_refresh').click(function(){ - _this.reader_file_list({path:_this.file_path}); - }); - - // 上传 - $('.file_nav_view .upload_or_download').on('click',function(e){ - _this.open_upload_view(); - e.stopPropagation(); - e.preventDefault(); - }); - - // 上传与下载下拉点击 - $('.file_nav_view .upload_or_download li').on('click',function(e){ - var type = $(this).data('type') - if(type === 'uploadFile'){ - _this.open_upload_view(); - }else{ - _this.open_download_view(); - } - e.stopPropagation(); - e.preventDefault(); - }); - - // 打开硬盘挂载的目录 - $('.mount_disk_list').on('click','.nav_btn',function(){ - var path = $(this).data('menu'); - _this.reader_file_list({path:path}); - }); - - // 表头点击事件,触发排序字段和排序方式 - $('.file_list_header').on('click','.file_name,.file_size,.file_mtime,.file_accept,.file_user', function (e) { - var _tid = $(this).attr('data-tid'), - _reverse = $(this).find('.icon_sort').hasClass('active'), - _active = $(this).hasClass('active'); - if (!$(this).find('.icon_sort').hasClass('active') && $(this).hasClass('active')) { - $(this).find('.icon_sort').addClass('active'); - }else{ - $(this).find('.icon_sort').removeClass('active'); - } - $(this).addClass('active').siblings().removeClass('active').find('.icon_sort').removeClass('active').empty(); - $(this).find('.icon_sort').html(''); - if (!_active) _reverse = true - bt.set_cookie('files_sort', _tid); - bt.set_cookie('name_reverse', _reverse ? 1 : 0); - _this.reader_file_list(); - return false; - }); - - // 设置排序显示 - $('.file_list_header .file_th').each(function (index, item) { - var files_sort = bt.get_cookie('files_sort'), - name_reverse = bt.get_cookie('name_reverse'); - if ($(this).attr('data-tid') === files_sort){ - $(this).addClass('active').siblings().removeClass('active').find('.icon_sort').removeClass('active').empty(); - $(this).find('.icon_sort').html(''); - if (name_reverse === 0) $(this).find('.icon_sort').addClass('active'); - } - }); - - // 全选选中文件 - $('.file_list_header .file_check').on('click', function (e){ - var checkbox = parseInt($(this).data('checkbox')); - switch(checkbox){ - case 0: - $(this).addClass('active').removeClass('active_2').data('checkbox',1); - $('.file_list_content .file_tr').addClass('active').removeClass('active_2'); - _this.file_table_arry = _this.file_list; - break; - case 2: - $(this).addClass('active').removeClass('active_2').data('checkbox',1); - $('.file_list_content .file_tr').addClass('active'); - _this.file_table_arry = _this.file_list; - break; - case 1: - $(this).removeClass('active active_2').data('checkbox',0); - $('.file_list_content .file_tr').removeClass('active'); - _this.file_table_arry = []; - break; - } - }); - - // 选中文件 - $('.file_list_content').on('click', '.file_check', function (e) { //列表选择 - var _tr = $(this).parents('.file_tr'),index = _tr.data('index'),header_check = $('.file_list_header .file_check'),filename = _tr.attr('data-filename'); - $('.selection_right_menu').removeAttr('style'); - if(_tr.hasClass('active')){ - _tr.removeClass('active'); - _this.remove_check_file(_this.file_table_arry,'filename',filename); - if(_this.file_table_arry.length > 0){ - header_check.addClass('active_2').removeClass('active').data('checkbox',2); - }else if(_this.file_table_arry.length == 0){ - header_check.removeClass('active active_2').data('checkbox',0); - } - }else{ - _tr.addClass('active'); - _tr.attr('data-filename',_this.file_list[index]['filename']); - if(_this.file_table_arry.length == _this.file_list.length){ - header_check.addClass('active').removeClass('active_2').data('checkbox',1); - }else{ - header_check.addClass('active_2').removeClass('active').data('checkbox',2); - } - _this.file_table_arry.push(_this.file_list[index]); - } - e.stopPropagation(); - }); - - // 文件列表滚动条事件 - $('.file_list_content').scroll(function(e){ - if($(this).scrollTop() == ($(this)[0].scrollHeight - $(this)[0].clientHeight)){ - $(this).prev().css('opacity',1); - $(this).next().css('opacity',0); - }else if($(this).scrollTop() > 0){ - $(this).prev().css('opacity',1); - }else if($(this).scrollTop() == 0){ - $(this).prev().css('opacity',0); - $(this).next().css('opacity',1); - } - }); - - // 选中文件 - $('.file_table_view .file_list_content').on('click','.file_tr',function(e){ - $('.selection_right_menu').removeAttr('style'); - $(this).find('.file_checkbox .file_check').click(); - e.stopPropagation(); - e.preventDefault(); - }); - - // 打开文件夹或文件 --- 文件名单击 - $('.file_table_view .file_list_content').on('click','.file_title i',function(e){ - var file_tr = $(this).parents('.file_tr'),index = file_tr.data('index'),data = _this.file_list[index]; - if(data.type == 'dir'){ - _this.reader_file_list({path:_this.file_path + '/' + data['filename']}); - }else{ - - } - e.stopPropagation(); - }); - - // 打开文件的分享状态 - $('.file_table_view .file_list_content').on('click','.file_name .icon-share1',function(e){ - var file_tr = $(this).parents('.file_tr'),index = file_tr.data('index'),data = _this.file_list[index]; - _this.info_file_share(data,file_tr); - e.stopPropagation(); - }); - - - // 打开文件的收藏夹状态 - $('.file_table_view .file_list_content').on('click','.file_name .icon-favorites',function(e){ - var file_tr = $(this).parents('.file_tr'),index = file_tr.data('index'),data = _this.file_list[index]; - data.typeText = data.type?'文件夹':'文件'; - _this.cancel_file_favorites(data,file_tr); - e.stopPropagation(); - }); - - - // 打开文件夹和文件 --- 双击 - $('.file_table_view .file_list_content').on('dblclick','.file_tr',function(e){ - if($(e.target).hasClass('file_check')) return false; - var index = $(this).data('index'),data = _this.file_list[index]; - if(data.type == 'dir'){ - - _this.reader_file_list({path:_this.file_path + '/' + data['filename']}); - }else{ - - } - e.stopPropagation(); - e.preventDefault(); - }); - - // 文件夹和文件鼠标右键 - $('.file_list_content').on('contextmenu','.file_tr',function(ev){ - var _that = this; - if(ev.which == 3){ - _this.render_file_groud_menu(ev,this); - $('.content_right_menu').removeAttr('style'); - $(this).addClass('selected').siblings().removeClass('selected'); - $(document).one('click',function(e){ - $(_that).removeClass('selected'); - $('.selection_right_menu').removeAttr('style'); - e.stopPropagation(); - e.preventDefault(); - }); - } - ev.stopPropagation(); - ev.preventDefault(); - }); - - // 文件空白区域右键菜单 - $('.file_list_content').on('contextmenu',function(ev){ - var content_right_menu = $('.content_right_menu'),content_menu_width = content_right_menu.width(),content_menu_height = content_right_menu.height(),_that = this; - if(ev.which == 3){ - $('.selection_right_menu').removeAttr('style'); - _this.render_file_all_menu(ev,this) - } - ev.stopPropagation(); - ev.preventDefault(); - }); - - //设置单页显示的数量,默认为100,设置local本地缓存 - $('.filePage').on('change','.showRow',function(){ - var val = $(this).val(); - bt.set_storage('showRow',val); - _this.reader_file_list(); - }); - - // 获取文件夹大小 - $('.file_list_content').on('click','.folder_size',function(e){ - var data = _this.file_list[$(this).parents('.file_tr').data('index')],_this = this; - _this.get_file_size({path:data.path},function(res){ - $(_this).text(bt.format_size(res.size)); - }); - e.stopPropagation(); - e.preventDefault(); - }); - - // 视图调整 - $('.cut_view_model').on('click',function(){ - var type = $(this).data('type'); - $('.file_table_view').addClass(type == 'icon'?'icon_view':'list_view').removeClass(type != 'icon'?'icon_view':'list_view'); - $(this).addClass('active').siblings().removeClass('active'); - }); - - }, - /** - * @description 设置文件前进或后退状态 - * @returns void - */ - set_file_forward:function(){ - var _this = this,forward_path = $('.forward_path span'); - console.log(_this.file_operation) - if(_this.file_operating.length == 1){ - forward_path.addClass('active'); - }else if(_this.file_pointer == _this.file_operating.length -1){ - forward_path.eq(0).removeClass('active'); - forward_path.eq(1).addClass('active'); - }else if(_this.file_pointer == 0){ - forward_path.eq(0).addClass('active'); - forward_path.eq(1).removeClass('active'); - }else{ - forward_path.removeClass('active'); - } - }, - /** - * @description 设置文件视图 - * @returns void - */ - set_file_view:function(){ - var file_list_content = $('.file_list_content'),height = this.area[1] - 170; - $('.file_bodys').height(this.area[1] - 100); - if((this.file_list.length * 50) > height){ - file_list_content.attr('data-height',file_list_content.data('height') || file_list_content.height()).height(height); - $('.file_shadow_bottom').css('opacity',1); - }else{ - file_list_content.height(height); - $('.file_shadow_top,.file_shadow_bottom').css('opacity',0); - } - }, - /** - * @description 清除表格选中数据和样式 - * @returns void - */ - clear_table_active:function(){ - this.file_table_arry = []; - $('.file_list_header .file_check').removeClass('active active_2'); - $('.file_list_content .file_tr').removeClass('active'); - }, - } - select_file.reader_view(); - select_file.event_bind(); - }, - /** - * @description 固定表头 - * @param {string} el DOM选择器 - * @return void - */ - $fixed_table_thead:function(el){ - $(el).scroll(function(){ - var scrollTop = this.scrollTop; - this.querySelector('thead').style.transform = 'translateY(' + scrollTop + 'px)'; - }); - }, - /** * @description 请求封装 * @param {string|object} conifg ajax配置参数/请求地址 @@ -3037,110 +1962,124 @@ var bt_tools = { * @param {function} callback1 回调函数/可为空 * @returns void 无 */ - send:function(param1,param2,param3,param4,param5,param6){ - var params = {},success = null,error = null,config = [],param_one = ''; - $.each(arguments,function(index,items){ - config.push([items,typeof items]); + send: function (param1, param2, param3, param4, param5, param6) { + var params = {}, + success = null, + error = null, + config = [], + param_one = ''; + $.each(arguments, function (index, items) { + config.push([items, typeof items]); }); - function diff_data(i){ + function diff_data(i) { try { - success = config[i][1] == "function"?config[i][0]:null; - error = config[(i+1)][1] == "function"?config[(i+1)][0]:null; - } catch (error){} + success = config[i][1] == "function" ? config[i][0] : null; + error = config[(i + 1)][1] == "function" ? config[(i + 1)][0] : null; + } catch (error) {} } param_one = config[0]; - switch(param_one[1]){ + switch (param_one[1]) { case "string": - $.each(config,function(index,items){ + $.each(config, function (index, items) { var value = items[0],type = items[1]; - if(index > 1 && (type == "boolean" || type == "string" || type == "object")){ + if (index > 1 && (type == "boolean" || type == "string" || type == "object")) { var arry = param_one[0].split('/'); - params['url'] = '/'+ arry[0] +'?action=' + arry[1]; - params['load'] = value; - if(type == "object"){ + params['url'] = '/' + arry[0] + '?action=' + arry[1]; + if (type == "object"){ params['load'] = value.load; - params['tips'] = value.tips; params['verify'] = value.verify; + if(value.plugin) params['url'] = '/plugin?action=a&name='+ arry[0] + '&s=' + arry[1]; + }else if(type == 'string'){ + params['load'] = value; } return false; - }else{ + } else { params['url'] = param_one[0]; } }); - if(config[1][1] === "object"){ + if (config[1][1] === "object") { params['data'] = config[1][0]; diff_data(2); - }else{ + } else { diff_data(1); } break; case 'object': params['url'] = param_one[0].url; params['data'] = param_one[0].data || {}; - $.each(config,function(index,items){ + $.each(config, function (index, items) { var value = items[0],type = items[1]; - if(index > 1 && (type == "boolean" || type == "string" || type == "object")){ - params['load'] = items; - if(type == "object"){ - params['load'] = value.load; - params['tips'] = value.tips; + if (index > 1 && (type == "boolean" || type == "string" || type == "object")) { + switch (type){ + case "object": + params['load'] = value.load; + params['verify'] = value.verify; + break; + case "string": + params['load'] = value; + break; } return true; } }); - if(config[1][1] === "object"){ + if (config[1][1] === "object") { params['data'] = config[1][0]; diff_data(2); - }else{ + } else { diff_data(1); } break; } - if(params.load) params.load = this.load(params.load); + if (params.load) params.load = this.load(params.load); $.ajax({ - type:params.type|| "POST", - url:params.url, - data:params.data || {}, + type: params.type || "POST", + url: params.url, + data: params.data || {}, dataType: params.dataType || "JSON", - complete:function(){ - if(params.load) params.load.close(); + complete: function (res) { + if (params.load) params.load.close(); }, - success:function (res){ - if(params.verify){ - if(success) success(res); + success: function (res) { + if (typeof params.verify == "boolean" && !params.verify) { + if (success) success(res); return false; } - if(typeof res === "string"){ - layer.msg(res,{icon:2,time:0,closeBtn:2}); + if (typeof res === "string") { + layer.msg(res, { + icon: 2, + time: 0, + closeBtn: 2 + }); return false; } - if(params.bacth){ - if(success) success(res); + if (params.batch) { + if (success) success(res); return false; } - if(res.status === false){ - bt_tools.msg(res); + if (res.status === false && (res.hasOwnProperty('msg') || res.hasOwnProperty('error_msg'))) { + if(error){ + error(res) + }else{ + bt_tools.msg({status:res.status,msg:!res.hasOwnProperty('msg')?res.error_msg:res.msg}); + } return false; } - if(params.tips){ + + if (params.tips) { bt_tools.msg(res); } - if(success) success(res); + if (success) success(res); } - // error:function(er){ - // if(error) error(er); - // layer.closeAll('dialog'); - // layer.closeAll('loading'); - // layer.msg('Service response error:'+ er.status +'
          error msg:'+er.statusText+'
          URL:' + params.url+'
          param:' + JSON.stringify(params.data) ,{icon:2,time:0,closeBtn:2}); - // } }); }, + /** * @description 命令行输入 */ command_line_output:function(config){ var _that = this,uuid = bt.get_random(15); + /** * @description 渲染 * @param config @@ -3186,7 +2125,9 @@ var bt_tools = { element_detection:null, uuid:uuid, fragment:[], - error:0, + error:0, // 错误次数,用于监听元素内容是否还存在 + retry:0, // 重试次数 + forceExit:false, // 强制断开连接 /** * @description 程序初始化 */ @@ -3218,12 +2159,13 @@ var bt_tools = { delete _that.commandConnectionPool[oldUUID]; } bt.set_cookie('commandInputViewUUID',this.uuid); - this.element_detection = setInterval(function(){ + this.element_detection = setInterval(function(){ if(!$(that.config.el).length){ clearInterval(that.element_detection) + that.forceExit = true; that.close_connect(); } - },1 * 60 *1000) + },1 * 60 * 1000) this.set_full_screen() this.create_websocket_connect(this.config.route,this.config.shell) this.monitor_element() @@ -3244,12 +2186,16 @@ var bt_tools = { this.send(JSON.stringify(this.socketToken)) this.send(shell) if(that.open) that.open() + that.retry = 0 }); this.socket.addEventListener('close',function(ev){ - if(ev.code !== 1000){ - that.socket = that.create_websocket_connect(that.config.route,that.config.shell) + if(!that.forceExit){ + if(ev.code !== 1000 && that.retry <= 10){ + that.socket = that.create_websocket_connect(that.config.route,that.config.shell) + that.retry ++; + } + if(that.close) that.close(ev) } - if(that.close) that.close(ev) }); this.socket.addEventListener('message',function(ws_event){ var result = ws_event.data @@ -3268,11 +2214,22 @@ var bt_tools = { // 1 }, + htmlEncodeByRegExp:function (str){ + if(str.length == 0) return ""; + return str.replace(/&/g,"&") + .replace(//g,">") + .replace(/ /g," ") + .replace(/\'/g,"'") + .replace(/\"/g,"""); + }, + /** * @description 刷新Pre数据 * @param {object} data 需要插入的数据 - */ + */ refresh_data:function(data){ + var data = this.htmlEncodeByRegExp(data) this.fragment.push(data) if(this.fragment.length >= 300){ this.fragment.splice(0,150) @@ -3308,6 +2265,85 @@ var bt_tools = { } this.commandConnectionPool[uuid] = new ReaderCommand(config) return this.commandConnectionPool[uuid] + }, + + + + /** + * @description 清理验证提示样式 + * @param {object} element 元素节点 + */ + $clear_verify_tips:function(element){ + element.removeClass('bt-border-error bt-border-sucess'); + layer.close('tips'); + }, + + /** + * @description 验证提示 + * @param {object} element 元素节点 + * @param {string} tips 警告提示 + * @return void + */ + $verify_tips:function(element,tips,is_error){ + if(typeof is_error === "undefined") is_error = true; + element.removeClass('bt-border-error bt-border-sucess').addClass(is_error?'bt-border-error':'bt-border-sucess'); + element.focus(); + layer.tips(''+ tips +"",element,{tips: [1,is_error?'red':'#20a53a'],time:3000,area:element.width()}); + }, + /** + * @description 验证值是否存在 + * @param {String} value 内容/值 + * @param {String|Boolean} attr 属性 + * @param {String} type 属性 + */ + $verify: function (value, attr, type) { + if (!value) return ''; + if (type === true) return value ? ' ' + attr : ''; + if (type === 'style') return attr ? attr + ':' + value + ';' : value; + return attr ? ' ' + attr + '="' + value + '"' : ' ' + value; + }, + + /** + * @description 批量操作的结果 + * @param {*} config + */ + $batch_success_table: function (config) { + var _that = this,length = $(config.html).length; + bt.open({ + type: 1, + title: config.title, + area: config.area || ['400px', '350px'], + shadeClose: false, + closeBtn: 2, + content: config.content || '
          ' + config.title + ' '+lan['public'].success+'
          ' + config.html + '
          ' + config.th + ''+lan['public'].result+'
          ', + success: function () { + if(length > 4) _that.$fixed_table_thead('.fiexd_thead'); + } + }); + }, + /** + * @description 固定表头 + * @param {string} el DOM选择器 + * @return void + */ + $fixed_table_thead:function(el) { + $(el).scroll(function () { + var scrollTop = this.scrollTop; + this.querySelector('thead').style.transform = 'translateY(' + scrollTop + 'px)'; + }); + }, + /** + * @description 插件视图设置 + * @param {object|string} layid dom元素或layer_id + * @param {object} config 插件宽度高度或其他配置 + */ + $piugin_view_set:function(layid,config){ + var element = $(typeof layid === "string"?('#layui-layer' + layid):layid).hide(),win = $(window); + setTimeout(function(){ + var width = config.width || element.width(),height = config.height || element.height(); + element.css($.extend(config,{left:((win.width()-width)/2),top:((win.height()-height)/2)})).addClass('custom_layer'); + },50); + setTimeout(function(){ element.show(); },500) } }; $.fn.serializeObject = function () { @@ -3320,5 +2356,10 @@ $.fn.serializeObject = function () { }, {}); }; - - +function arryCopy(arrys){ + var list = arrys.concat(),arry = [] + for (var i = 0; i < list.length; i++) { + arry.push($.extend(true,{},list[i])) + } + return arry +} \ No newline at end of file diff --git a/BTPanel/static/language/English/lan.js b/BTPanel/static/language/English/lan.js index b4084aae..64c0b2c2 100644 --- a/BTPanel/static/language/English/lan.js +++ b/BTPanel/static/language/English/lan.js @@ -1006,7 +1006,7 @@ var lan = { "mysql_status_ps4":"If too low, increase innodb_buffer_pool_size", "mysql_status_ps5":"If too low, increase query_cache_size", "mysql_status_ps6":"If too high, increase tmp_table_size", - "mysql_status_ps7":"If too high, increase table_open_cache", + "mysql_status_ps7":"If too high, increase table_cache_size", "mysql_status_ps8":"If not 0, please check index of database table", "mysql_status_ps9":"If not 0, please check index of database table", "mysql_status_ps10":"If too high, increase sort_buffer_size", @@ -1513,7 +1513,7 @@ var lan = { "deploy_success_cret": "Remind: ", "try_renew_cret": "The SSL will be auto renewed 1 month before the expiration date", "cert_brand": "Brand: ", - "expire_time": "Expire: ", + "expire_time": "Expire date: ", "auth_domain": "Certified domain(s): ", "ssl_renew": "Renew", "deploy_success_tips": "Successfully deployed: Please replace the new certificate before the certificate expires", diff --git a/BTPanel/static/language/English/public.json b/BTPanel/static/language/English/public.json index 38f066fa..fb2c0eb6 100644 --- a/BTPanel/static/language/English/public.json +++ b/BTPanel/static/language/English/public.json @@ -238,7 +238,7 @@ "MOVE_ERR":"Failed to move file or directory!", "FILE_GET_ERR":"Failed to obtain file content, format is not supported!", "FILE_SAVE_SUCCESS":"Saved!", - "FILE_SAVE_ERR":"Save ERROR!", + "FILE_SAVE_ERR":"Save ERROR! {1}", "ZIP_SUCCESS":"Compression succeeded!", "ZIP_ERR":"Compression failed!", "UNZIP_SUCCESS":"Uncompression succeeded!", @@ -350,7 +350,11 @@ "MAX_PROCESSES": "Max processes", "MAX_CONNECTIONS": "Max connections", "NOT_LIMITED_BY_0": "0 means not limited", - "MAX_SPARE_SERVERS": "Max spare servers", + "MAX_SPARE_THREADS": "Maximum number of idle threads", + "MIN_SPARE_THREADS": "Minimum number of idle threads available to handle request spikes", + "THREADS_PER_CHILD": "Number of threads created by each child process", + "MAX_REQUEST_WORKERS": "Maximum number of connections that will be processed simultaneously", + "MaxConnectionsPerChild": "Limit on the number of connections that an individual child server will handle during its life", "CONFIG_ERROR": "Configuration ERROR", "SECRET_KEY_CHECK_FALSE": "Secret key verification failed", "CHECK_IP_FALSE": "IP verification failed, your access IP is", @@ -1235,5 +1239,4 @@ "BACKUP_MSG": "Backup to {1}, a total of {3} {2}, and failures {4}.", "BACKUP_MSG1": "Backup failed {1}/total {2} sites", "NOTIFICATION": "Notification" - } diff --git a/BTPanel/templates/default/config.html b/BTPanel/templates/default/config.html index 0d921976..92986e68 100644 --- a/BTPanel/templates/default/config.html +++ b/BTPanel/templates/default/config.html @@ -114,7 +114,7 @@ Notification
          - Set + Set
          diff --git a/BTPanel/templates/default/database.html b/BTPanel/templates/default/database.html index 19f4eda2..db2954d2 100644 --- a/BTPanel/templates/default/database.html +++ b/BTPanel/templates/default/database.html @@ -1,15 +1,7 @@ {% extends "layout.html" %} {% block content %} - +
          @@ -17,40 +9,22 @@ {{data['lan']['H1']}}/{{data['lan']['H2']}}
          -

          After adding the database, be sure to [Cron] page adds scheduled backup tasks to ensure your data security.

          -
          - - - - - - - - -
          -
          - -
          -
          -
          -
          -
          - {{data['lan']['SP1']}} - {{data['lan']['SP2']}} - {{data['lan']['SP3']}} -
          +

          + After adding the database, be sure to [Cron] page adds scheduled backup tasks to ensure your data security. +

          +
          -
          - {% endblock %} {% block scripts %} @@ -72,7 +45,7 @@ layer.msg('{{data["lan"]["JS1"]}}{{data["lan"]["JS2"]}}',{icon:7,shade: [0.3, '#000'],time:0}); $(".layui-layer-shade").css("margin-left", "180px"); {% else %} - database.get_list(); + // database.get_list(); {% endif %} diff --git a/BTPanel/templates/default/error3.html b/BTPanel/templates/default/error3.html new file mode 100644 index 00000000..ef9dce4c --- /dev/null +++ b/BTPanel/templates/default/error3.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% block content %} +
          +
          +
          +
          +

          Sorry, you need to meet the following conditions to use:

          +

          Please upgrade Nginx firewall version to 7.8.0 or above

          +
          +
          +
          +
          +{% endblock %} +{% block scripts %} +{% endblock %} \ No newline at end of file diff --git a/BTPanel/templates/default/files.html b/BTPanel/templates/default/files.html index 986e1f9b..49442d96 100644 --- a/BTPanel/templates/default/files.html +++ b/BTPanel/templates/default/files.html @@ -59,6 +59,11 @@ + - - - - - - - - - - - - + + + + + + + + + + + + + {% for js_f in g['other_js'] %} + + {% endfor %} {% block scripts %}{% endblock %} + \ No newline at end of file diff --git a/BTPanel/templates/default/site.html b/BTPanel/templates/default/site.html index 93871696..eb5290cf 100644 --- a/BTPanel/templates/default/site.html +++ b/BTPanel/templates/default/site.html @@ -29,7 +29,19 @@

          {{data['lan']['PS']}}after the site is successfully established, please[Cron]add scheduled backup tasks to the page!

          -
          +
          + PHP Project + Node Project +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          @@ -37,6 +49,25 @@ {% block scripts %} + @@ -44,23 +75,6 @@ {% endblock %} \ No newline at end of file diff --git a/class/acme_v2.py b/class/acme_v2.py index 4caa1960..806b72ad 100644 --- a/class/acme_v2.py +++ b/class/acme_v2.py @@ -82,7 +82,7 @@ class acme_v2: return self._apis # 尝试从云端获取 - res = requests.get(self._url) + res = requests.get(self._url,verify=False) if not res.status_code in [200, 201]: result = res.json() if "type" in result: @@ -754,7 +754,8 @@ fullchain.pem Paste into certificate input box # 替换服务器上的同域名同品牌证书 def sub_all_cert(self, key_file, pem_file): cert_init = self.get_cert_init(pem_file) # 获取新证书的基本信息 - paths = ['vhost/cert', 'vhost/ssl'] + paths = ['/www/server/panel/vhost/cert', '/www/server/panel/vhost/ssl','/www/server/panel'] + is_panel = False for path in paths: if not os.path.exists(path): continue @@ -765,7 +766,12 @@ fullchain.pem Paste into certificate input box to_info = to_path + '/info.json' # 判断目标证书是否存在 if not os.path.exists(to_pem_file): - continue + if not p_name in ['ssl']: continue + to_pem_file = to_path + '/certificate.pem' + to_key_file = to_path + '/privateKey.pem' + if not os.path.exists(to_pem_file): + continue + is_panel = True # 获取目标证书的基本信息 to_cert_init = self.get_cert_init(to_pem_file) # 判断证书品牌是否一致 @@ -793,6 +799,7 @@ fullchain.pem Paste into certificate input box write_log(public.getMsg('ACME_CERT_REPLACE',(to_path,))) # 重载web服务 public.serviceReload() + if is_panel: public.restart_panel() # 检查指定证书是否在订单列表 def check_order_exists(self, pem_file): @@ -1251,8 +1258,6 @@ fullchain.pem Paste into certificate input box def apply_cert(self, domains, auth_type='dns', auth_to='Dns_com|None|None', **args): write_log("", "wb+") try: - if 'auto_wildcard' in args and args['auto_wildcard']: - self._auto_wildcard = True self.get_apis() index = None if 'index' in args: @@ -1289,23 +1294,42 @@ fullchain.pem Paste into certificate input box # 申请证书 - api def apply_cert_api(self, args): # 是否为指定站点 - if re.match(r"^\d+$", args.auth_to): - import panelSite - path = public.M('sites').where('id=?',(args.id,)).getField('path') - args.auth_to = path + '/' + panelSite.panelSite().GetRunPath(args) - args.auth_to = args.auth_to.replace("//","/") - if args.auth_to[-1] == '/': - args.auth_to = args.auth_to[:-1] + if public.M('sites').where('id=? and project_type=?', (args.id, 'Java')).count(): + project_info = public.M('sites').where('id=?', (args.id,)).getField('project_config') + try: + project_info = json.loads(project_info) + if not 'ssl_path' in project_info: + return public.returnMsg(False, 'There is a problem with the current Java project configuration file, please rebuild') + if not os.path.exists(project_info['ssl_path']): + os.makedirs(project_info['ssl_path']) + path = project_info['ssl_path'] + args.auth_to=path + check_result = self.check_auth_env(args) + if check_result: return check_result - if not os.path.exists(args.auth_to): - return public.returnMsg(False, 'ACME_DIR_ERR') - - check_result = self.check_auth_env(args) - if check_result: return check_result - - if args.auto_wildcard == '1': - self._auto_wildcard = True - return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) + if args.auto_wildcard == '1': + self._auto_wildcard = True + return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) + except: + return public.returnMsg(False, 'There is a problem with the current Java project configuration file, please rebuild') + else: + if re.match(r"^\d+$", args.auth_to): + import panelSite + path = public.M('sites').where('id=?', (args.id,)).getField('path') + args.auth_to = path + '/' + panelSite.panelSite().GetRunPath(args) + args.auth_to = args.auth_to.replace("//", "/") + if args.auth_to[-1] == '/': + args.auth_to = args.auth_to[:-1] + + if not os.path.exists(args.auth_to): + return public.returnMsg(False, 'ACME_DIR_ERR') + + check_result = self.check_auth_env(args) + if check_result: return check_result + + if args.auto_wildcard == '1': + self._auto_wildcard = True + return self.apply_cert(json.loads(args.domains), args.auth_type, args.auth_to) #检查认证环境 def check_auth_env(self,args): @@ -1458,6 +1482,7 @@ fullchain.pem Paste into certificate input box if self._config['orders'][i]['cert_timeout'] > s_time or self._config['orders'][i]['auth_to'] == 'dns': continue if self.find_site_stopped(self._config['orders'][i]['domains']) == '0': + write_log("|-The website has been suspended, skip certificate renewal!") continue #已删除的网站直接跳过续签 @@ -1466,6 +1491,21 @@ fullchain.pem Paste into certificate input box auth_to = self.get_ssl_used_site(self._config['orders'][i]['save_path']) if not auth_to: continue self._config['orders'][i]['auth_to'] = auth_to + + # 是否到了允许重试的时间 + if 'next_retry_time' in self._config['orders'][i]: + timeout = self._config['orders'][i]['next_retry_time'] - int(time.time()) + if timeout > 0: + write_log('|-The domain name skipped this time: {}, because the last renewal failed, you still need to wait {} hours and try again'.format(self._config['orders'][index]['domains'],int(timeout / 60 / 60))) + continue + + # 是否到了最大重试次数 + if 'retry_count' in self._config['orders'][i]: + if self._config['orders'][i]['retry_count'] >= 3: + write_log('|-Skip the domain name this time: {}, this certificate will not be renewed due to failure to renew 3 times in a row'.format(self._config['orders'][index]['domains'])) + continue + + # 加入到续签订单 order_index.append(i) if not order_index: @@ -1500,11 +1540,27 @@ fullchain.pem Paste into certificate input box write_log(public.getMsg('ACME_DOWNLOAD_CERT')) cert = self.download_cert(index) self._config['orders'][index]['renew_time'] = int(time.time()) + + # 清理失败重试记录 + self._config['orders'][index]['retry_count'] = 0 + self._config['orders'][index]['next_retry_time'] = 0 + + # 保存证书配置 self.save_config() cert['status'] = True cert['msg'] = public.getMsg('ACME_RENEW_SUCCESS') write_log(public.getMsg('ACME_RENEW_SUCCESS1')) except Exception as e: + if str(e).find('请稍候重试') == -1: # 受其它证书影响和连接CA失败的的不记录重试次数 + # 设置下次重试时间 + self._config['orders'][index]['next_retry_time'] = int(time.time() + (86400 * 2)) + # 记录重试次数 + if not 'retry_count' in self._config['orders'][index].keys(): + self._config['orders'][index]['retry_count'] = 1 + self._config['orders'][index]['retry_count'] += 1 + # 保存证书配置 + self.save_config() + write_log("|-" + str(e).split('>>>>')[0]) write_log("-" * 70) return cert diff --git a/class/ajax.py b/class/ajax.py index 9b819929..c4bcd551 100644 --- a/class/ajax.py +++ b/class/ajax.py @@ -333,7 +333,38 @@ class ajax: def get_load_average(self,get): data = public.M('load_average').dbfile('system').where("addtime>=? AND addtime<=?",(get.start,get.end)).field('id,pro,one,five,fifteen,addtime').order('id asc').select() return self.ToAddtime(data) - + + + def get_process_tops(self,get): + ''' + @name 获取进程开销排行 + @author hwliang<2021-09-07> + @param get{ + start: int<开始时间> + end: int<结束时间> + } + @return list + ''' + data = public.M('process_tops').dbfile('system').where("addtime>=? AND addtime<=?",(get.start,get.end)).field('id,process_list,addtime').order('id asc').select() + return self.ToAddtime(data) + + + def get_process_cpu_high(self,get): + ''' + @name 获取CPU占用高的进程列表 + @author hwliang<2021-09-07> + @param get{ + start: int<开始时间> + end: int<结束时间> + } + @return list + ''' + data = public.M('process_high_percent').dbfile('system').where("addtime>=? AND addtime<=?",(get.start,get.end)).field('id,name,pid,cmdline,cpu_percent,memory,cpu_time_total,addtime').order('id asc').select() + return self.ToAddtime(data) + + + + def ToAddtime(self,data,tomem = False): import time @@ -349,12 +380,16 @@ class ajax: if length > 10000: he = 15 if he == 1: for i in range(length): - data[i]['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(data[i]['addtime']))) - if tomem and data[i]['mem'] > 100: data[i]['mem'] = data[i]['mem'] / mPre - if tomem in [None]: - if type(data[i]['down_packets']) == str: - data[i]['down_packets'] = json.loads(data[i]['down_packets']) - data[i]['up_packets'] = json.loads(data[i]['up_packets']) + try: + data[i]['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(data[i]['addtime']))) + if 'process_list' in data[i]: + data[i]['process_list'] = json.loads(data[i]['process_list']) + if tomem and data[i]['mem'] > 100: data[i]['mem'] = data[i]['mem'] / mPre + if tomem in [None]: + if type(data[i]['down_packets']) == str: + data[i]['down_packets'] = json.loads(data[i]['down_packets']) + data[i]['up_packets'] = json.loads(data[i]['up_packets']) + except: continue return data else: count = 0 @@ -363,14 +398,16 @@ class ajax: if count < he: count += 1 continue - value['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(value['addtime']))) - if tomem and value['mem'] > 100: value['mem'] = value['mem'] / mPre - if tomem in [None]: - if type(value['down_packets']) == str: - value['down_packets'] = json.loads(value['down_packets']) - value['up_packets'] = json.loads(value['up_packets']) - tmp.append(value) - count = 0 + try: + value['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(value['addtime']))) + if tomem and value['mem'] > 100: value['mem'] = value['mem'] / mPre + if tomem in [None]: + if type(value['down_packets']) == str: + value['down_packets'] = json.loads(value['down_packets']) + value['up_packets'] = json.loads(value['up_packets']) + tmp.append(value) + count = 0 + except: continue return tmp def GetInstalleds(self,softlist): @@ -578,19 +615,25 @@ class ajax: phpini = public.readFile(filename) data = {} rep = "disable_functions\s*=\s{0,1}(.*)\n" - tmp = re.search(rep,phpini).groups() - data['disable_functions'] = tmp[0] + + tmp = re.search(rep,phpini) + if tmp: + data['disable_functions'] = tmp.groups()[0] rep = "upload_max_filesize\s*=\s*([0-9]+)(M|m|K|k)" - tmp = re.search(rep,phpini).groups() - data['max'] = tmp[0] + + tmp = re.search(rep,phpini) + if tmp: + data['max'] = tmp.groups()[0] rep = u"\n;*\s*cgi\.fix_pathinfo\s*=\s*([0-9]+)\s*\n" - tmp = re.search(rep,phpini).groups() - if tmp[0] == '0': - data['pathinfo'] = False - else: - data['pathinfo'] = True + tmp = re.search(rep,phpini) + if tmp: + if tmp.groups()[0] == '0': + data['pathinfo'] = False + else: + data['pathinfo'] = True + self.getCloudPHPExt(get) phplib = json.loads(public.readFile('data/phplib.conf')) libs = [] @@ -1176,7 +1219,7 @@ class ajax: if os.path.exists(u_path): os.remove(u_path) return public.returnMsg(False,'AJAX_USER_BE_OVERDUE') url_headers = {"authorization":"bt {}".format(userInfo['token'])} - resp = requests.post('{}/api/user/verifyToken'.format(self.__official_url),headers=url_headers) + resp = requests.post('{}/api/user/verifyToken'.format(self.__official_url),headers=url_headers,verify=False) resp = resp.json() if not resp['success']: if os.path.exists(u_path): os.remove(u_path) @@ -1185,6 +1228,7 @@ class ajax: session[m_key] = public.returnMsg(True,'AJAX_USER_IS_VALID') return session[m_key] + #PHP探针 def php_info(self,args): php_version = args.php_version.replace('.','') diff --git a/class/common.py b/class/common.py index dfc8270d..a8a68ff0 100644 --- a/class/common.py +++ b/class/common.py @@ -26,7 +26,7 @@ class panelSetup: ua = g.ua.lower() if ua.find('spider') != -1 or g.ua.find('bot') != -1: return redirect('https://www.google.com') - g.version = '6.8.14' + g.version = '6.8.16' g.title = public.GetConfigValue('title') g.uri = request.path g.debug = os.path.exists('data/debug.pl') @@ -44,6 +44,11 @@ class panelSetup: g.cdn_url = '/static' session['title'] = g.title + g.recycle_bin_open = 0 + if os.path.exists("data/recycle_bin.pl"): g.recycle_bin_open = 1 + + g.recycle_bin_db_open = 0 + if os.path.exists("data/recycle_bin_db.pl"): g.recycle_bin_db_open = 1 g.is_aes = False self.other_import() return None @@ -150,31 +155,27 @@ class panelAdmin(panelSetup): g.api_request = True else: if session['login'] == False: - public.WriteLog('Login auth', 'The current session has been logged out') session.clear() return redirect('/login') if 'tmp_login_expire' in session: s_file = 'data/session/{}'.format(session['tmp_login_id']) if session['tmp_login_expire'] < time.time(): - public.WriteLog('Login auth', 'Temporary authorization has expired {}'.format(public.get_client_ip())) session.clear() if os.path.exists(s_file): os.remove(s_file) return redirect('/login') if not os.path.exists(s_file): - public.WriteLog('Login auth', 'Forced withdrawal due to cancellation of temporary authorization {}'.format(public.get_client_ip())) session.clear() return redirect('/login') ua_md5 = public.md5(g.ua) if ua_md5 != session.get('login_user_agent',ua_md5): - public.WriteLog('Login auth', 'UA verification failed {}'.format(public.get_client_ip())) session.clear() return redirect('/login') if api_check: + now_time = time.time() session_timeout = session.get('session_timeout',0) - if session_timeout < time.time() and session_timeout != 0: - public.WriteLog('Login auth', 'The session has expired {}'.format(public.get_client_ip())) + if session_timeout < now_time and session_timeout != 0: session.clear() return redirect('/login?dologin=True&go=0') @@ -182,16 +183,17 @@ class panelAdmin(panelSetup): login_token = session.get('login_token','') if login_token: if login_token != public.get_login_token_auth(): - public.WriteLog('Login auth', 'Session ID does not match {}'.format(public.get_client_ip())) session.clear() return redirect('/login?dologin=True&go=1') - if api_check: - filename = 'data/sess_files/' + public.get_sess_key() - if not os.path.exists(filename): - public.WriteLog('Login auth', 'Trigger CSRF defense {}'.format(public.get_client_ip())) - session.clear() - return redirect('/login?dologin=True&go=2') + # if api_check: + # filename = 'data/sess_files/' + public.get_sess_key() + # if not os.path.exists(filename): + # session.clear() + # return redirect('/login?dologin=True&go=2') + + # 标记新的会话过期时间 + session['session_timeout'] = time.time() + public.get_session_timeout() except: public.WriteLog('Login auth',public.get_error_info()) session.clear() diff --git a/class/config.py b/class/config.py index cce72277..c0ec2ec3 100644 --- a/class/config.py +++ b/class/config.py @@ -55,6 +55,14 @@ class config: else: return public.returnMsg(True, 'EMAIL_NOT_EXISTS') + def del_tg_info(self,get): + import panel_telegram_bot + return panel_telegram_bot.panel_telegram_bot().del_tg_bot(get) + + def set_tg_bot(self,get): + import panel_telegram_bot + return panel_telegram_bot.panel_telegram_bot().set_tg_bot(get) + #添加接受邮件地址 def add_mail_address(self, get): if not hasattr(get, 'email'): return public.returnMsg(False, 'INPUT_EMAIL') @@ -139,6 +147,15 @@ class config: ret['user_mail'] = {"user_name": user_mail, "mail_list": self.__mail_list,"info":self.get_user_mail(get)} ret['dingding'] = {"dingding": dingding,"info":self.get_dingding(get)} return ret + + def get_settings2(self, get=None): + import panel_telegram_bot + tg = panel_telegram_bot.panel_telegram_bot() + tg = tg.get_tg_conf() + conf = self.get_settings() + conf['telegram'] = tg + return conf + # 设置钉钉报警 def set_dingding(self, get): if not (hasattr(get, 'url') or hasattr(get, 'atall')): @@ -179,7 +196,8 @@ class config: userInfo = public.M('users').where("id=?",(1,)).field('username,password').find() token = public.Md5(userInfo['username'] + '/' + userInfo['password']) public.writeFile(self._setup_path+'/data/login_token.pl',token) - + skey = 'login_token' + cache.set(skey,token) sess_path = 'data/sess_files' if not os.path.exists(sess_path): os.makedirs(sess_path,384) @@ -286,6 +304,9 @@ class config: def setPanel(self,get): if not public.IsRestart(): return public.returnMsg(False,'EXEC_ERR_TASK') + if 'limitip' in get: + if get.limitip.find('/') != -1: + return public.returnMsg(False,'The authorized IP format is incorrect, and the subnet segment writing is not supported') isReWeb = False sess_out_path = 'data/session_timeout.pl' if 'session_timeout' in get: @@ -733,8 +754,6 @@ class config: #设置面板SSL def SetPanelSSL(self,get): - ssl_path = "{}/ssl".format(public.get_panel_path()) - if not os.path.exists(ssl_path): os.makedirs(ssl_path,384) if hasattr(get,"email"): #rep_mail = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$" rep_mail = r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" @@ -748,6 +767,7 @@ class config: sslConf = self._setup_path+'/data/ssl.pl' if os.path.exists(sslConf): public.ExecShell('rm -f ' + sslConf) + g.rm_ssl = True return public.returnMsg(True,'PANEL_SSL_CLOSE') else: public.ExecShell('pip install cffi') diff --git a/class/crontab.py b/class/crontab.py index 64516d93..07a64b9e 100644 --- a/class/crontab.py +++ b/class/crontab.py @@ -121,6 +121,7 @@ class crontab: def set_cron_status(self,get): id = get['id'] cronInfo = public.M('crontab').where('id=?',(id,)).field(self.field).find() + status_msg = ['Stop','Start'] status = 1 if cronInfo['status'] == status: status = 0 @@ -130,7 +131,7 @@ class crontab: self.sync_to_crond(cronInfo) public.M('crontab').where('id=?',(id,)).setField('status',status) - public.WriteLog('TYPE_CRON',"MODIFY_CRON_STATUS",(cronInfo['name'],str(status))) + public.WriteLog('TYPE_CRON',"MODIFY_CRON_STATUS",(cronInfo['name'],str(status_msg[status]))) return public.returnMsg(True,'SET_SUCCESS') #修改计划任务 @@ -290,6 +291,7 @@ class crontab: data['orderOpt'] = [] import json tmp = public.readFile('data/libList.conf') + if not tmp: return data libs = json.loads(tmp) for lib in libs: if not 'opt' in lib: continue diff --git a/class/data.py b/class/data.py index 2f65f8fe..ec8c74bf 100644 --- a/class/data.py +++ b/class/data.py @@ -294,7 +294,14 @@ class data: where = type_where else: where += " and " + type_where - + if get.table == 'sites': + if where: + where = "({}) AND project_type='PHP'".format(where) + else: + where = "project_type='PHP'" + + + field = self.GetField(get.table) #实例化数据库对象 diff --git a/class/database.py b/class/database.py index 94a86bc1..e6634eed 100644 --- a/class/database.py +++ b/class/database.py @@ -10,7 +10,7 @@ #------------------------------ # 数据库管理类 #------------------------------ -import public,db,re,time,os,sys,panelMysql +import public,db,re,time,os,sys,panelMysql,json from BTPanel import session import datatool class database(datatool.datatools): @@ -30,7 +30,7 @@ class database(datatool.datatools): ssl = get.ssl if ssl == "REQUIRE SSL" and not self.check_mysql_ssl_status(get): return public.returnMsg(False,'MYSQL_SSL_ERR') - data_name = get['name'].strip() + data_name = get['name'].strip().lower() if self.CheckRecycleBin(data_name): return public.returnMsg(False,'DATABASE_DEL_RECYCLE_BIN',(data_name,)) if len(data_name) > 64: return public.returnMsg(False, 'DATABASE_NAME_LEN') reg = r"^[\w\.-]+$" @@ -332,7 +332,20 @@ SetLink panelMysql.panelMysql().execute("drop user '" + username + "'@'" + us[0] + "'") panelMysql.panelMysql().execute("flush privileges") rPath = '/www/Recycle_bin/' - public.writeFile(rPath + 'BTDB_' + name +'_t_' + str(time.time()),json.dumps(data)) + data['rmtime'] = int(time.time()) + rm_path = '{}/BTDB_{}_t_{}'.format(rPath,name,data['rmtime']) + if os.path.exists(rm_path): rm_path += '.1' + rm_config_file = '{}/config.json'.format(rm_path) + datadir = public.get_datadir() + db_path = '{}/{}'.format(datadir,name) + if not os.path.exists(db_path): + return public.returnMsg(False,'Means that the database data does not exist!') + + public.ExecShell("mv -f {} {}".format(db_path,rm_path)) + if not os.path.exists(rm_path): + return public.returnMsg(False,'Failed to move database data to the recycle bin!') + public.writeFile(rm_config_file,json.dumps(data)) + # public.writeFile(rPath + 'BTDB_' + name +'_t_' + str(time.time()),json.dumps(data)) public.M('databases').where("name=?",(name,)).delete() public.WriteLog("TYPE_DATABASE", 'DATABASE_DEL_SUCCESS',(name,)) return public.returnMsg(True,'RECYCLE_BIN_DB') @@ -340,39 +353,66 @@ SetLink #永久删除数据库 def DeleteTo(self,filename): import json - data = json.loads(public.readFile(filename)) - if public.M('databases').where("name=?",( data['name'],)).count(): + if os.path.isfile(filename): + data = json.loads(public.readFile(filename)) + if public.M('databases').where("name=?",( data['name'],)).count(): + os.remove(filename) + return public.returnMsg(True,'DEL_SUCCESS') + result = panelMysql.panelMysql().execute("drop database `" + data['name'] + "`") + isError=self.IsSqlError(result) + if isError != None: return isError + panelMysql.panelMysql().execute("drop user '" + data['username'] + "'@'localhost'") + users = panelMysql.panelMysql().query("select Host from mysql.user where User='" + data['username'] + "' AND Host!='localhost'") + for us in users: + panelMysql.panelMysql().execute("drop user '" + data['username'] + "'@'" + us[0] + "'") + panelMysql.panelMysql().execute("flush privileges") os.remove(filename) - return public.returnMsg(True,'DEL_SUCCESS') - result = panelMysql.panelMysql().execute("drop database `" + data['name'] + "`") - isError=self.IsSqlError(result) - if isError != None: return isError - panelMysql.panelMysql().execute("drop user '" + data['username'] + "'@'localhost'") - users = panelMysql.panelMysql().query("select Host from mysql.user where User='" + data['username'] + "' AND Host!='localhost'") - for us in users: - panelMysql.panelMysql().execute("drop user '" + data['username'] + "'@'" + us[0] + "'") - panelMysql.panelMysql().execute("flush privileges") - os.remove(filename) - public.WriteLog("TYPE_DATABASE", 'DATABASE_DEL_SUCCESS',(data['name'],)) + else: + import shutil + if os.path.exists(filename): + data = json.loads(public.readFile(filename + '/config.json')) + shutil.rmtree(filename) + try: + public.WriteLog("TYPE_DATABASE", 'DATABASE_DEL_SUCCESS',(data['name'],)) + except: + pass return public.returnMsg(True,'DEL_SUCCESS') #恢复数据库 def RecycleDB(self,filename): import json - data = json.loads(public.readFile(filename)) + _isdir = False + if os.path.isfile(filename): + data = json.loads(public.readFile(filename)) + else: + re_config_file = filename + '/config.json' + data = json.loads(public.readFile(re_config_file)) + db_path = "{}/{}".format(public.get_datadir(),data['name']) + if os.path.exists(db_path): + return public.returnMsg(False,'There is a database with the same name in the current database. To ensure data security, stop recovery!') + _isdir = True + if public.M('databases').where("name=?",( data['name'],)).count(): - os.remove(filename) + if not _isdir: os.remove(filename) return public.returnMsg(True,'RECYCLEDB') + + if not _isdir: + os.remove(filename) + else: + public.ExecShell('mv -f {} {}'.format(filename,db_path)) + if not os.path.exists(db_path): + return public.returnMsg(False,'Data recovery failed!') + db_config_file = "{}/config.json".format(db_path) + if os.path.exists(db_config_file): os.remove(db_config_file) + + # 设置文件权限 + public.ExecShell("chown -R mysql:mysql {}".format(db_path)) + public.ExecShell("chmod -R 660 {}".format(db_path)) + public.ExecShell("chmod 700 {}".format(db_path)) + self.__CreateUsers(data['name'],data['username'],data['password'],data['accept']) - #result = panelMysql.panelMysql().execute("grant all privileges on `" + data['name'] + "`.* to '" + data['username'] + "'@'localhost' identified by '" + data['password'] + "'") - #isError=self.IsSqlError(result) - #if isError != None: return isError - #panelMysql.panelMysql().execute("grant all privileges on `" + data['name'] + "`.* to '" + data['username'] + "'@'" + data['accept'] + "' identified by '" + data['password'] + "'") - #panelMysql.panelMysql().execute("flush privileges") - public.M('databases').add('id,pid,name,username,password,accept,ps,addtime',(data['id'],data['pid'],data['name'],data['username'],data['password'],data['accept'],data['ps'],data['addtime'])) - os.remove(filename) return public.returnMsg(True,"RECYCLEDB") #设置ROOT密码 @@ -965,49 +1005,54 @@ SetLink else: return public.returnMsg(False,"CHANVE_FAIL") - # 检查用户管理表是否存在 - def _check_table_exist(self): - result = public.M('sqlite_master').where("name=?", ('mysql_user',)).getField('name') - if not result: - self._create_mysql_user_tb() + def get_average_num(self,slist): + """ + @获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size - # 数据库对象 - def _get_sqlite_connect(self): - import sqlite3 - try: - if not self.sqlite_connection: - self.sqlite_connection = sqlite3.connect('data/default.db') - except Exception as ex: - return "error: " + str(ex) + if len(slist) > 1: + slist = sorted(slist) + limit_size =int((slist[0] + slist[-1])/2 * 0.85) + return limit_size - # 创多用户表 - def _create_mysql_user_tb(self): - self._get_sqlite_connect() - sql=""" -CREATE TABLE mysql_user( - id INTEGER PRIMARY KEY AUTOINCREMENT, - pid INTEGER , - username CHAR, - password CHAR, - accept CHAR, - ps CHAR, - addtime CHAR -);""" - self.sqlite_connection.execute(sql) + def get_database_size(self,get): + """ + 获取数据库大小 + """ + result = {} + tables = public.get_database_size() + data = public.M('databases').field('id,pid,name,ps,addtime').select() + public.print_log(data) + for x in data: + name = x['name'] + x['total'] = 0 + x['backup_count'] = public.M('backup').where("pid=? AND type=?",(x['id'],'1')).count() + if name in tables: x['total'] = tables[name] - # 根据id获取用户列表 - def get_mysql_user(self,get): - self._create_mysql_user_tb() - result = public.M('mysql_user').where("pid=?", (get.id,)).select() + result[name] = x return result - def add_mysql_user(self,get): - ''' - * 添加mysql用户 - * @param get.id 面板数据库id - * @param get.username 添加的用户 - * @param get.password 添加的用户密码 - * @param get.permission 添加的用户权限 - * @param get.host 允许在哪里访问 - * @return Bool - ''' + def check_del_data(self,get): + """ + @删除数据库前置检测 + """ + ids = json.loads(get.ids) + slist = {};result = [];db_list_size = [] + db_data = self.get_database_size(None) + for key in db_data: + data = db_data[key] + if not data['id'] in ids: continue + + db_addtime = public.to_date(times = data['addtime']) + data['score'] = int(time.time() - db_addtime) + data['total'] + data['st_time'] = db_addtime + + if data['total'] > 0 : db_list_size.append(data['total']) + result.append(data) + + slist['data'] = sorted(result,key= lambda x:x['score'],reverse=True) + slist['db_size'] = self.get_average_num(db_list_size) + return slist \ No newline at end of file diff --git a/class/db_mysql.py b/class/db_mysql.py new file mode 100644 index 00000000..60ec895f --- /dev/null +++ b/class/db_mysql.py @@ -0,0 +1,337 @@ +#coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: hwliang +# +------------------------------------------------------------------- +import re, os, sys, public, json +import pymysql + + +class mysql: + __DB_PASS = '' + __DB_USER = '' + __DB_NAME = '' + __DB_PORT = 3306 + __DB_HOST = 'localhost' + __DB_PREFIX = '' + __DB_CONN = None + __DB_CUR = None + __DB_ERR = None + __DB_NET = None + __DB_TABLE = "" # 被操作的表名称 + __OPT_WHERE = "" # where条件 + __OPT_LIMIT = "" # limit条件 + __OPT_ORDER = "" # order条件 + __OPT_FIELD = "*" # field条件 + __OPT_PARAM = () # where值 + + def __init__(self): + pass + + def set_name(self, name): + self.__DB_NAME = name + return self + + def set_host(self, host, port, name, username, password, prefix=''): + self.__DB_HOST = host + self.__DB_PORT = port + self.__DB_NAME = name + self.__DB_USER = username + self.__DB_PASS = password + self.__DB_PREFIX = prefix + return self + + #连接MYSQL数据库 + def __GetConn(self): + if self.__DB_NET: return True + try: + self.__DB_CONN = pymysql.connect(host=self.__DB_HOST, + port=self.__DB_PORT, + user=self.__DB_USER, + passwd=self.__DB_PASS) + + self.__DB_CUR = self.__DB_CONN.cursor() + self.__DB_NET = True + return True + except pymysql.Error as e: + self.__DB_ERR = e + return False + + def table(self, table): + #设置表名 + self.__DB_TABLE = self.__DB_PREFIX + table + return self + + def where(self, where, param): + #WHERE条件 + if where: + self.__OPT_WHERE = " WHERE " + where + self.__OPT_PARAM = self.__to_tuple(param) + return self + + def __to_tuple(self, param): + #将参数转换为tuple + if type(param) != tuple: + if type(param) == list: + param = tuple(param) + else: + param = (param, ) + return param + + def order(self, order): + #ORDER条件 + if len(order): + self.__OPT_ORDER = " ORDER BY " + order + return self + + def limit(self, limit): + #LIMIT条件 + limit = str(limit) + if len(limit): + self.__OPT_LIMIT = " LIMIT " + limit + return self + + def field(self, field): + #FIELD条件 + if len(field): + self.__OPT_FIELD = field + return self + + def select(self): + #查询数据集 + self.__GetConn() + try: + self.__get_columns() + sql = "SELECT " + self.__OPT_FIELD + " FROM " + self.__DB_TABLE + self.__OPT_WHERE + self.__OPT_ORDER + self.__OPT_LIMIT + self.__DB_CUR.execute(sql, self.__OPT_PARAM) + data = self.__DB_CUR.fetchall() + #构造字典系列 + if self.__OPT_FIELD != "*": + fields = self.__format_field(self.__OPT_FIELD.split(',')) + tmp = [] + for row in data: + i = 0 + tmp1 = {} + for key in fields: + tmp1[key.strip('`')] = row[i] + i += 1 + tmp.append(tmp1) + del (tmp1) + data = tmp + del (tmp) + else: + #将元组转换成列表 + tmp = list(map(list, data)) + data = tmp + del (tmp) + self.__close() + return data + except Exception as ex: + return public.get_error_info() + + def get(self): + self.__get_columns() + return self.select() + + def __format_field(self, field): + import re + fields = [] + for key in field: + s_as = re.search(r'\s+as\s+', key, flags=re.IGNORECASE) + if s_as: + as_tip = s_as.group() + key = key.split(as_tip)[1] + fields.append(key) + return fields + + def __get_columns(self): + if self.__OPT_FIELD == '*': + tmp_cols = self.query( + "select COLUMN_NAME from information_schema.COLUMNS where table_name = '{}' and table_schema = '{}';" + .format(self.__DB_TABLE, self.__DB_NAME), False) + cols = [] + for col in tmp_cols: + cols.append('`' + col[0] + '`') + if len(cols) > 0: self.__OPT_FIELD = ','.join(cols) + + def getField(self, keyName): + #取回指定字段 + try: + result = self.field(keyName).select() + if len(result) != 0: + return result[0][keyName] + return result + except: + return None + + def setField(self, keyName, keyValue): + #更新指定字段 + return self.save(keyName, (keyValue, )) + + def find(self): + #取一行数据 + try: + result = self.limit("1").select() + if len(result) == 1: + return result[0] + return result + except: + return None + + def count(self): + #取行数 + key = "COUNT(*)" + data = self.field(key).select() + try: + return int(data[0][key]) + except: + return 0 + + def add(self, keys, param): + #插入数据 + self.__GetConn() + self.__DB_CONN.text_factory = str + try: + values = "" + for key in keys.split(','): + values += "%s," + values = values[0:len(values) - 1] + sql = "INSERT INTO " + self.__DB_TABLE + "(" + keys + ") " + "VALUES(" + values + ")" + self.__DB_CUR.execute(sql, self.__to_tuple(param)) + id = self.__DB_CUR.lastrowid + self.__close() + self.__DB_CONN.commit() + return id + except Exception as ex: + return "error: " + str(ex) + + #插入数据 + def insert(self, pdata): + if not pdata: return False + keys, param = self.__format_pdata(pdata) + return self.add(keys, param) + + #更新数据 + def update(self, pdata): + if not pdata: return False + keys, param = self.__format_pdata(pdata) + return self.save(keys, param) + + #构造数据 + def __format_pdata(self, pdata): + keys = pdata.keys() + keys_tmp = [] + for k in keys: + keys_tmp.append("`{}`".format(k)) + keys_str = ','.join(keys_tmp) + + param = [] + for k in keys: + #if pdata[k] == None: pdata[k] = '' + param.append(pdata[k]) + return keys_str, tuple(param) + + def addAll(self, keys, param): + #插入数据 + self.__GetConn() + self.__DB_CONN.text_factory = str + try: + values = "" + for key in keys.split(','): + values += "%s," + values = values[0:len(values) - 1] + sql = "INSERT INTO " + self.__DB_TABLE + "(" + keys + ") " + "VALUES(" + values + ")" + result = self.__DB_CUR.execute(sql, self.__to_tuple(param)) + return True + except Exception as ex: + return "error: " + str(ex) + + def commit(self): + self.__close() + self.__DB_CONN.commit() + + def save(self, keys, param): + #更新数据 + self.__GetConn() + self.__DB_CONN.text_factory = str + try: + opt = "" + for key in keys.split(','): + opt += key + "=%s," + opt = opt[0:len(opt) - 1] + sql = "UPDATE " + self.__DB_TABLE + " SET " + opt + self.__OPT_WHERE + + #处理拼接WHERE与UPDATE参数 + tmp = list(self.__to_tuple(param)) + for arg in self.__OPT_PARAM: + tmp.append(arg) + self.__OPT_PARAM = tuple(tmp) + self.__DB_CUR.execute(sql, self.__OPT_PARAM) + self.__close() + self.__DB_CONN.commit() + return self.__DB_CUR.rowcount + except Exception as ex: + return "error: " + str(ex) + + def delete(self, id=None): + #删除数据 + self.__GetConn() + try: + if id: + self.__OPT_WHERE = " WHERE id=%s" + self.__OPT_PARAM = (id, ) + sql = "DELETE FROM " + self.__DB_TABLE + self.__OPT_WHERE + self.__DB_CUR.execute(sql, self.__OPT_PARAM) + self.__close() + self.__DB_CONN.commit() + return self.__DB_CUR.rowcount + except Exception as ex: + return "error: " + str(ex) + + def execute(self, sql, is_close=True): + #执行SQL语句返回受影响行 + if not self.__GetConn(): return self.__DB_ERR + try: + result = self.__DB_CUR.execute(sql) + self.__DB_CONN.commit() + if is_close: self.__close() + return result + except Exception as ex: + return ex + + def query(self, sql, is_close=True): + #执行SQL语句返回数据集 + if not self.__GetConn(): return self.__DB_ERR + try: + self.__DB_CUR.execute(sql) + result = self.__DB_CUR.fetchall() + #将元组转换成列表 + data = list(map(list, result)) + if is_close: self.__Close() + return data + except Exception as ex: + return ex + + #关闭连接 + def __Close(self): + self.__DB_CUR.close() + self.__DB_CONN.close() + + def __close(self): + #清理条件属性 + self.__OPT_WHERE = "" + self.__OPT_FIELD = "*" + self.__OPT_ORDER = "" + self.__OPT_LIMIT = "" + self.__OPT_PARAM = () + + def close(self): + #释放资源 + try: + self.__DB_CUR.close() + self.__DB_CUR.close() + except: + pass \ No newline at end of file diff --git a/class/files.py b/class/files.py index bbf012a6..75f9ec48 100644 --- a/class/files.py +++ b/class/files.py @@ -335,6 +335,11 @@ session.save_handler = files'''.format(path, sess_path, sess_path) get.path = get.path.encode('utf-8') if get.path == '': get.path = '/www' + + # 转换包含~的路径 + if get.path.find('~') != -1: + get.path = os.path.expanduser(get.path) + get.path = self.xssdecode(get.path) if not os.path.exists(get.path): get.path = '/www/wwwroot' @@ -925,6 +930,8 @@ session.save_handler = files'''.format(path, sess_path, sess_path) else: tmp['size'] = os.path.getsize(fname) if os.path.isdir(fname): + if file[:5] == 'BTDB_': + tmp['size'] = public.get_path_size(fname) data['dirs'].append(tmp) else: data['files'].append(tmp) @@ -2410,6 +2417,22 @@ cd %s attribute['history'] = self.get_history_info(filename) # 历史文件 return attribute + def files_search(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_search(args) + + + def files_replace(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_replace(args) + + def get_replace_logs(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_replace_logs(args) + # 数据库对象 def _get_sqlite_connect(self): try: diff --git a/class/flask_compress.py b/class/flask_compress.py index 73b0a7fa..9e6adb5b 100644 --- a/class/flask_compress.py +++ b/class/flask_compress.py @@ -1,8 +1,8 @@ -import sys +import sys,os from gzip import GzipFile from io import BytesIO -from flask import request, current_app,session,Response +from flask import request, current_app,session,Response,g if sys.version_info[:2] == (2, 6): @@ -79,6 +79,32 @@ class Compress(object): app = self.app or current_app accept_encoding = request.headers.get('Accept-Encoding', '') response.headers['Server'] = 'nginx' + response.headers['Connection'] = 'keep-alive' + + if 'dologin' in g and app.config['SSL']: + try: + for k,v in request.cookies.items(): + response.set_cookie(k,'',expires='Thu, 01-Jan-1970 00:00:00 GMT',path='/') + except: + pass + + if 'rm_ssl' in g: + import public + try: + for k,v in request.cookies.items(): + response.set_cookie(k,'',expires='Thu, 01-Jan-1970 00:00:00 GMT',path='/') + except: + pass + session_name = app.config['SESSION_COOKIE_NAME'] + session_id = public.get_session_id() + response.set_cookie(session_name,'',expires='Thu, 01-Jan-1970 00:00:00 GMT',path='/') + response.set_cookie(session_name, session_id, path='/', max_age=86400 * 30,httponly=True) + + request_token = request.cookies.get('request_token','') + if request_token: + response.set_cookie('request_token',request_token,path='/',max_age=86400 * 30) + + if (response.mimetype not in app.config['COMPRESS_MIMETYPES'] or 'gzip' not in accept_encoding.lower() or not 200 <= response.status_code < 300 or diff --git a/class/http_requests.py b/class/http_requests.py index c7d69b22..e7f4977d 100644 --- a/class/http_requests.py +++ b/class/http_requests.py @@ -275,7 +275,7 @@ exit($header."\r\n\r\n".json_encode($body)); #取CURL路径 def _curl_bin(self): - c_bin = ['/usr/local/curl2/bin/curl','/usr/local/curl/bin/curl','/usr/bin/curl'] + c_bin = ['/usr/local/curl2/bin/curl','/usr/local/curl/bin/curl','/usr/local/bin/curl','/usr/bin/curl'] for cb in c_bin: if os.path.exists(cb): return cb return 'curl' diff --git a/class/jobs.py b/class/jobs.py index 60355d15..8c70d124 100644 --- a/class/jobs.py +++ b/class/jobs.py @@ -6,7 +6,7 @@ # +------------------------------------------------------------------- # | Author: hwliang # +------------------------------------------------------------------- -import time,public,db,os,sys,json,re +import time,public,db,os,sys,json,re,shutil os.chdir('/www/server/panel') def control_init(): @@ -37,9 +37,17 @@ def control_init(): )''' sql.execute(csql,()) if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%type_id%')).count(): - public.M('sites').execute("alter TABLE sites add edate integer DEFAULT '0000-00-00'",()) public.M('sites').execute("alter TABLE sites add type_id integer DEFAULT 0",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%edate%')).count(): + public.M('sites').execute("alter TABLE sites add edate integer DEFAULT '0000-00-00'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%project_type%')).count(): + public.M('sites').execute("alter TABLE sites add project_type STRING DEFAULT 'PHP'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%project_config%')).count(): + public.M('sites').execute("alter TABLE sites add project_config STRING DEFAULT '{}'",()) + sql = db.Sql() if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'site_types')).count(): csql = '''CREATE TABLE IF NOT EXISTS `site_types` ( @@ -137,6 +145,9 @@ def control_init(): public.ExecShell("chmod -R 600 /www/server/panel/data;chmod -R 600 /www/server/panel/config;chmod -R 700 /www/server/cron;chmod -R 600 /www/server/cron/*.log;chown -R root:root /www/server/panel/data;chown -R root:root /www/server/panel/config;chown -R root:root /www/server/phpmyadmin;chmod -R 755 /www/server/phpmyadmin") if os.path.exists("/www/server/mysql"): public.ExecShell("chown mysql:mysql /etc/my.cnf;chmod 600 /etc/my.cnf") + public.ExecShell("rm -rf /www/server/panel/temp/*") + if not public.is_debug(): + public.ExecShell("rm -f /www/server/panel/class/pluginAuth.py") stop_path = '/www/server/stop' if not os.path.exists(stop_path): os.makedirs(stop_path) @@ -164,11 +175,119 @@ def control_init(): #check_firewall() check_dnsapi() clean_php_log() - #update_py37() files_set_mode() set_pma_access() # public.set_open_basedir() clear_fastcgi_safe() + update_py37() + run_script() + set_php_cli_env() + + +def set_php_cli_env(): + ''' + @name 设置php-cli环境变量 + @author hwliang<2021-09-07> + @return void + ''' + php_path = '/www/server/php' + bashrc = '/root/.bashrc' + if not os.path.exists(php_path): return + if not os.path.exists(bashrc): return + # 清理所有别名 + public.ExecShell('sed -i "/alias php/d" {}'.format(bashrc)) + bashrc_body = public.readFile(bashrc) + if not bashrc_body: return + + # 设置默认环境变量版本别名 + env_php_bin = '/usr/bin/php' + if os.path.exists(env_php_bin): + if os.path.islink(env_php_bin): + env_bin_version = os.readlink(env_php_bin).split('/')[-3] + php_cli_ini = "{}/{}/etc/php-cli.ini".format(php_path,env_bin_version) + bashrc_body += "alias php='php -c {}'\n".format(php_cli_ini) + + + # 设置所有已安装的PHP版本环境变量和别名 + php_versions_list = ['52','53','54','55','56','70','71','72','73','74','80','81','82','83','84','90','91'] + for php_version in php_versions_list: + php_ini = "{}/{}/etc/php.ini".format(php_path,php_version) + php_cli_ini = "{}/{}/etc/php-cli.ini".format(php_path,php_version) + env_php_bin = "/usr/bin/php{}".format(php_version) + php_bin = "{}/{}/bin/php".format(php_path,php_version) + php_ize = '/usr/bin/php{}-phpize'.format(php_version) + php_ize_src = "{}/{}/bin/phpize".format(php_path,php_version) + php_fpm = '/usr/bin/php{}-php-fpm'.format(php_version) + php_fpm_src = "{}/{}/sbin/php-fpm".format(php_path,php_version) + php_pecl = '/usr/bin/php{}-pecl'.format(php_version) + php_pecl_src = "{}/{}/bin/pecl".format(php_path,php_version) + php_pear = '/usr/bin/php{}-pear'.format(php_version) + php_pear_src = "{}/{}/bin/pear".format(php_path,php_version) + + if os.path.exists(php_bin): + # 设置每个版本的环境变量 + if not os.path.exists(env_php_bin): os.symlink(php_bin,env_php_bin) + if not os.path.exists(php_ize) and os.path.exists(php_ize_src): os.symlink(php_ize_src,php_ize) + if not os.path.exists(php_fpm) and os.path.exists(php_fpm_src): os.symlink(php_fpm_src,php_fpm) + if not os.path.exists(php_pecl) and os.path.exists(php_pecl_src): os.symlink(php_pecl_src,php_pecl) + if not os.path.exists(php_pear) and os.path.exists(php_pear_src): os.symlink(php_pear_src,php_pear) + public.ExecShell("\cp -f {} {}".format(php_ini,php_cli_ini)) # 每次复制新的php.ini到php-cli.ini + public.ExecShell('sed -i "/disable_functions/d" {}'.format(php_cli_ini)) # 清理禁用函数 + bashrc_body += "alias php{}='php{} -c {}'\n".format(php_version,php_version,php_cli_ini) # 设置别名 + else: + # 清理已卸载的环境变量 + if os.path.exists(env_php_bin): os.remove(env_php_bin) + if os.path.exists(php_ize): os.remove(php_ize) + if os.path.exists(php_fpm): os.remove(php_fpm) + if os.path.exists(php_pecl): os.remove(php_pecl) + if os.path.exists(php_pear): os.remove(php_pear) + public.writeFile(bashrc,bashrc_body) + + +def write_run_script_log(_log,rn='\n'): + _log_file = '/www/server/panel/logs/run_script.log' + public.writeFile(_log_file,_log + rn,'a+') + + +def run_script(): + os.system("{} {}/script/run_script.py".format(public.get_python_bin(),public.get_panel_path())) + run_tip = '/dev/shm/bt.pl' + if os.path.exists(run_tip): return + public.writeFile(run_tip,str(time.time())) + uptime = int(public.readFile('/proc/uptime').split()[0]) + if uptime > 1800: return + run_config ='/www/server/panel/data/run_config' + script_logs = '/www/server/panel/logs/script_logs' + if not os.path.exists(run_config): + os.makedirs(run_config,384) + if not os.path.exists(script_logs): + os.makedirs(script_logs,384) + + for sname in os.listdir(run_config): + script_conf_file = '{}/{}'.format(run_config,sname) + if not os.path.exists(script_conf_file): continue + script_info = json.loads(public.readFile(script_conf_file)) + exec_log_file = '{}/{}'.format(script_logs,sname) + + if not os.path.exists(script_info['script_file']) \ + or script_info['script_file'].find('/www/server/panel/plugin/') != 0 \ + or not re.match('^\w+$',script_info['script_file']): + os.remove(script_conf_file) + if os.path.exists(exec_log_file): os.remove(exec_log_file) + continue + + + if script_info['script_type'] == 'python': + _bin = public.get_python_bin() + elif script_info['script_type'] == 'bash': + _bin = '/usr/bin/bash' + if not os.path.exists(_bin): _bin = 'bash' + + exec_script = 'nohup {} {} &> {} &'.format(_bin,script_info['script_file'],exec_log_file) + public.ExecShell(exec_script) + script_info['last_time'] = time.time() + public.writeFile(script_conf_file,json.dumps(script_info)) + def clear_fastcgi_safe(): try: diff --git a/class/panelBackup.py b/class/panelBackup.py index 78400b61..5e0d7ec8 100644 --- a/class/panelBackup.py +++ b/class/panelBackup.py @@ -216,6 +216,15 @@ class backup: if self._is_save_local: _not_save_local = False + pdata = { + 'type': '2', + 'name': spath, + 'pid': 0, + 'filename': dfile, + 'addtime': public.format_date(), + 'size': os.path.getsize(dfile) + } + public.M('backup').insert(pdata) if _not_save_local: if os.path.exists(dfile): os.remove(dfile) @@ -252,7 +261,7 @@ class backup: #尝试删除本地文件 if os.path.exists(backup['filename']): try: - os.remove(self._local_backdir + '/'+ data_type +'/' + backup['name']) + os.remove(backup['filename']) except: pass self.echo_info(public.getMsg("BACKUP_CLEAN",(backup['filename'],))) @@ -402,6 +411,16 @@ class backup: if self._is_save_local: _not_save_local = False + pdata = { + 'type': 0, + 'name': fname, + 'pid': pid, + 'filename': dfile, + 'addtime': public.format_date(), + 'size': os.path.getsize(dfile) + } + public.M('backup').insert(pdata) + if _not_save_local: if os.path.exists(dfile): os.remove(dfile) @@ -619,6 +638,16 @@ class backup: if self._is_save_local: _not_save_local = False + pdata = { + 'type': '1', + 'name': fname, + 'pid': pid, + 'filename': dfile, + 'addtime': public.format_date(), + 'size': os.path.getsize(dfile) + } + public.M('backup').insert(pdata) + if _not_save_local: if os.path.exists(dfile): os.remove(dfile) @@ -659,7 +688,7 @@ class backup: aaPanel reminds you that the cron you set failed to execute: * Server IP: {} * Time: {} - * Task name: {}{} + * Task name: {} {} * The following is a list of {} that failed to backup: {} @@ -667,7 +696,16 @@ class backup: Please deal with it as soon as possible to avoid unnecessary trouble due to the failure of the backup task. - Notification by aaPanel""".format( server_ip, now, task_name, remark, backup_type, msg) - return notice_content + tg_content = """📣‼*aaPanel reminds you that the cron failed to execute*‼ + +* Server IP*: {} +* Time*: {} +* Task name*: {} {} +* The following is a list of {} that failed to backup*: +{} +--Notification by aaPanel""".format( + server_ip, now, task_name, remark, backup_type, msg) + return {"mail":notice_content,"tg":tg_content} def generate_failture_notice(self, task_name, msg, remark): # from send_mail import send_mail @@ -681,7 +719,7 @@ class backup: aaPanel reminds you that the cron you set failed to execute: * Server IP: {} * Time: {} - * Task name:{}{} + * Task name: {}{} * Error messages: {} @@ -689,7 +727,17 @@ class backup: Please deal with it as soon as possible to avoid unnecessary trouble due to the failure of the backup task. -- Notification by aaPanel""".format( server_ip, now, task_name, remark, msg) - return notice_content + tg_content = """📣‼*aaPanel reminds you that the cron failed to execute*‼ + +* Server IP*: {} +* Time*: {} +* Task name*: {} {} +* Error messages*: +{} + +-- Notification by aaPanel""".format( + server_ip, now, task_name, remark, msg) + return {'mail':notice_content,'tg':tg_content} def get_cron_info(self, cron_name): """ 通过计划任务名称查找计划任务配置参数 """ @@ -778,9 +826,10 @@ class backup: else: self.echo_error(public.getMsg('NOTIFICATION_ERR')) - def send_notification(self, channel, title, msg = ""): + def send_notification(self, channel, title, msg = {}): try: from send_mail import send_mail + from config import config tondao = [] if channel.find(",") >= 0: tongdao = channel.split(",") @@ -788,14 +837,16 @@ class backup: tongdao = [channel] sm = send_mail() + c = config() send_res = [] error_count = 0 channel_names = { "mail": "email", - # "dingidng": "钉钉" + "telegram": "telegram" } error_channel = [] - settings = sm.get_settings() + # settings = sm.get_settings() + settings = c.get_settings2() for td in tongdao: _res = False if td == "mail": @@ -804,14 +855,15 @@ class backup: mail_list = settings['user_mail']['mail_list'] if len(mail_list) == 1: mail_list = mail_list[0] - _res = sm.qq_smtp_send(mail_list, title=title, body=msg.replace("\n", "
          ")) + _res = sm.qq_smtp_send(mail_list, title=title, body=msg['mail'].replace("\n", "
          ")) if not _res: error_count += 1 error_channel.append(channel_names[td]) - if td == "dingding": - if len(settings["dingding"]['info']) == 0: + if td == "telegram": + import panel_telegram_bot + if not settings["telegram"]['setup']: continue - _res = sm.dingding_send(msg) + _res = panel_telegram_bot.panel_telegram_bot().send_by_tg_bot(msg['tg']) send_res.append(_res) if not _res: error_count += 1 diff --git a/class/panelPlugin.py b/class/panelPlugin.py index 649625cb..485646ae 100644 --- a/class/panelPlugin.py +++ b/class/panelPlugin.py @@ -348,7 +348,7 @@ class panelPlugin: if 'token' in pdata: url_headers = {"authorization": "bt {}".format(pdata['token'])} pdata['environment_info'] = json.dumps(public.fetch_env_info()) - listTmp = requests.post(cloudUrl, params=pdata, headers=url_headers) + listTmp = requests.post(cloudUrl, params=pdata, headers=url_headers,verify=False) listTmp=listTmp.json() if not listTmp: listTmp = public.readFile(lcoalTmp) @@ -632,10 +632,11 @@ class panelPlugin: #处理分类 def get_types(self,sList,sType): if sType <= 0: return sList - if sType != 12: - sType = [sType] - else: - sType = [sType,8] + sType = [sType] + # if sType != 12: + # sType = [sType] + # else: + # sType = [sType,8] newList = [] for sInfo in sList: if sInfo['type'] in sType: newList.append(sInfo) @@ -682,7 +683,8 @@ class panelPlugin: soft_list_tmp = [] softList['list'] = self.check_isinstall(softList['list']) for val in softList['list']: - if val['setup']: soft_list_tmp.append(val) + if 'setup' in val: + if val['setup']: soft_list_tmp.append(val) softList['list'] = soft_list_tmp softList['list'] = self.get_page(softList['list'],get) else: @@ -804,38 +806,40 @@ class panelPlugin: def set_coexist(self,sList): softList = [] for sInfo in sList: - if sInfo['version_coexist'] == 1: - for versionA in sInfo['versions']: - sTmp = sInfo.copy() - v = versionA['m_version'].replace('.','') - sTmp['title'] = sTmp['title']+'-'+versionA['m_version'] - sTmp['name'] = sTmp['name']+'-'+versionA['m_version'] - sTmp['version'] = sTmp['version'].replace('{VERSION}',v) - sTmp['manager_version'] = sTmp['manager_version'].replace('{VERSION}',v) - sTmp['install_checks'] = sTmp['install_checks'].replace('{VERSION}',v) - if 'uninsatll_checks' not in sTmp: - sTmp['uninsatll_checks'] = sTmp['uninstall_checks'].replace('{VERSION}',v) - else: - sTmp['uninsatll_checks'] = sTmp['uninsatll_checks'].replace('{VERSION}',v) - sTmp['s_version'] = sTmp['s_version'].replace('{VERSION}',v) - sTmp['versions'] = [] - sTmp['versions'].append(versionA) - softList.append(sTmp) - else: - softList.append(sInfo) + try: + if sInfo['version_coexist'] == 1 and 'versions' in sInfo: + for versionA in sInfo['versions']: + try: + sTmp = sInfo.copy() + v = versionA['m_version'].replace('.','') + sTmp['title'] = sTmp['title']+'-'+versionA['m_version'] + sTmp['name'] = sTmp['name']+'-'+versionA['m_version'] + sTmp['version'] = sTmp['version'].replace('{VERSION}',v) + sTmp['manager_version'] = sTmp['manager_version'].replace('{VERSION}',v) + sTmp['install_checks'] = sTmp['install_checks'].replace('{VERSION}',v) + if 'uninsatll_checks' not in sTmp: + sTmp['uninsatll_checks'] = sTmp['uninstall_checks'].replace('{VERSION}',v) + else: + sTmp['uninsatll_checks'] = sTmp['uninsatll_checks'].replace('{VERSION}',v) + sTmp['s_version'] = sTmp['s_version'].replace('{VERSION}',v) + sTmp['versions'] = [] + sTmp['versions'].append(versionA) + softList.append(sTmp) + except: continue + else: + softList.append(sInfo) + except: continue return softList #检测是否安装 def check_isinstall(self,sList): - try: - if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') - indexList = json.loads(public.ReadFile(self.__index)) - for i in range(len(sList)): - sList[i]['index_display'] = sList[i]['name'] in indexList - sList[i] = self.check_status(sList[i]) - return sList - except: - public.writeFile(self.__index,'[]') + if not os.path.exists(self.__index): public.writeFile(self.__index,'[]') + indexList = json.loads(public.ReadFile(self.__index)) + for i in range(len(sList)): + sList[i]['index_display'] = sList[i]['name'] in indexList + sList[i] = self.check_status(sList[i]) + return sList + #检查软件状态 def check_status(self,softInfo): @@ -2077,7 +2081,7 @@ class panelPlugin: import panelPHP return panelPHP.panelPHP(get.name).exec_php_script(get) return public.returnMsg(False,'PLUGIN_INPUT_B') - if not self.check_accept(get):return public.returnMsg(False,public.to_string([24744, 26410, 36141, 20080, 91, 37, 115, 93, 25110, 25480, 26435, 24050, 21040, 26399, 33]) % (self.get_title_byname(get),)) + if not self.check_accept(get):return public.returnMsg(False,"You did not purchase [ %s ] or the authorization has expired" % (self.get_title_byname(get),)) public.package_path_append(path) plugin_main = __import__(get.name+'_main') try: diff --git a/class/panelProjectController.py b/class/panelProjectController.py new file mode 100644 index 00000000..818420ce --- /dev/null +++ b/class/panelProjectController.py @@ -0,0 +1,90 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# 宝塔Linux面板 +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# 项目管理控制器 +#------------------------------ +import os,sys,public,json,re + +class ProjectController: + + + def __init__(self): + pass + + def model(self,args): + ''' + @name 调用指定项目模型 + @author hwliang<2021-07-15> + @param args { + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON + } + ''' + try: # 表单验证 + if args['mod_name'] in ['base']: return public.return_status_code(1000,'错误的调用!') + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: return public.return_status_code(1000,'调用的方法名称中不能包含“__”字符') + if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,'调用的模块名称中不能包含\w以外的字符') + if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,'调用的方法名称中不能包含\w以外的字符') + except: + return public.get_error_object() + # 参数处理 + mod_name = "{}Model".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() + + # 指定模型是否存在 + mod_file = "{}/projectModel/{}.py".format(public.get_class_path(),mod_name) + if not os.path.exists(mod_file): + return public.return_status_code(1003,mod_name) + # 实例化 + def_object = public.get_script_object(mod_file) + if not def_object: return public.return_status_code(1000,'没有找到{}模型'.format(mod_name)) + run_object = getattr(def_object.main(),def_name,None) + if not run_object: return public.return_status_code(1000,'没有在{}模型中找到{}方法'.format(mod_name,def_name)) + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.get_error_object() + else: + pdata = args.data + else: + pdata = public.dict_obj() + + # 前置HOOK + hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) + hook_result = public.exec_hook(hook_index,pdata) + if isinstance(hook_result,public.dict_obj): + pdata = hook_result # 桥接 + elif isinstance(hook_result,dict): + return hook_result # 响应具体错误信息 + elif isinstance(hook_result,bool): + if not hook_result: # 直接中断操作 + return public.return_data(False,{},error_msg='前置HOOK中断操作') + + # 调用处理方法 + result = run_object(pdata) + + # 后置HOOK + hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) + hook_data = public.to_dict_obj({ + 'args': pdata, + 'result': result + }) + hook_result = public.exec_hook(hook_index,hook_data) + if isinstance(hook_result,dict): + result = hook_result['result'] + return result + + + \ No newline at end of file diff --git a/class/panelSSL.py b/class/panelSSL.py index ac154c26..7a5a4eda 100644 --- a/class/panelSSL.py +++ b/class/panelSSL.py @@ -283,7 +283,7 @@ class panelSSL: content = args.content import http_requests - res = http_requests.get(url,s_type='curl',timeout=6) + res = http_requests.get(url,s_type='curl',timeout=6,verify=False) result = res.text if not result: return 0 @@ -356,6 +356,7 @@ class panelSSL: #发送请求 def request(self,dname): self.__PDATA['data'] = json.dumps(self.__PDATA['data']) + result= public.returnMsg(False,'The request failed, please try again later!') try: result = public.httpPost(self.__APIURL2 + '/' + dname,self.__PDATA) result = json.loads(result) @@ -689,27 +690,88 @@ class panelSSL: #获取证书名称 def GetCertName(self,get): + return self.get_cert_init(get.certPath) + # try: + # openssl = '/usr/local/openssl/bin/openssl' + # if not os.path.exists(openssl): openssl = 'openssl' + # result = public.ExecShell(openssl + " x509 -in "+get.certPath+" -noout -subject -enddate -startdate -issuer") + # tmp = result[0].split("\n") + # data = {} + # data['subject'] = tmp[0].split('=')[-1] + # data['notAfter'] = self.strfToTime(tmp[1].split('=')[1]) + # data['notBefore'] = self.strfToTime(tmp[2].split('=')[1]) + # if tmp[3].find('O=') == -1: + # data['issuer'] = tmp[3].split('CN=')[-1] + # else: + # data['issuer'] = tmp[3].split('O=')[-1].split(',')[0] + # if data['issuer'].find('/') != -1: data['issuer'] = data['issuer'].split('/')[0] + # result = public.ExecShell(openssl + " x509 -in "+get.certPath+" -noout -text|grep DNS") + # data['dns'] = result[0].replace('DNS:','').replace(' ','').strip().split(',') + # return data + # except: + # print(public.get_error_info()) + # return None + + + # 获取指定证书基本信息 + def get_cert_init(self, pem_file): + if not os.path.exists(pem_file): + return None try: - openssl = '/usr/local/openssl/bin/openssl' - if not os.path.exists(openssl): openssl = 'openssl' - result = public.ExecShell(openssl + " x509 -in "+get.certPath+" -noout -subject -enddate -startdate -issuer") - tmp = result[0].split("\n") - data = {} - data['subject'] = tmp[0].split('=')[-1] - data['notAfter'] = self.strfToTime(tmp[1].split('=')[1]) - data['notBefore'] = self.strfToTime(tmp[2].split('=')[1]) - if tmp[3].find('O=') == -1: - data['issuer'] = tmp[3].split('CN=')[-1] + import OpenSSL + result = {} + x509 = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, public.readFile(pem_file)) + # 取产品名称 + issuer = x509.get_issuer() + result['issuer'] = '' + if hasattr(issuer, 'CN'): + result['issuer'] = issuer.CN + if not result['issuer']: + is_key = [b'0', '0'] + issue_comp = issuer.get_components() + if len(issue_comp) == 1: + is_key = [b'CN', 'CN'] + for iss in issue_comp: + if iss[0] in is_key: + result['issuer'] = iss[1].decode() + break + # 取到期时间 + result['notAfter'] = self.strf_date( + bytes.decode(x509.get_notAfter())[:-1]) + # 取申请时间 + result['notBefore'] = self.strf_date( + bytes.decode(x509.get_notBefore())[:-1]) + # 取可选名称 + result['dns'] = [] + for i in range(x509.get_extension_count()): + s_name = x509.get_extension(i) + if s_name.get_short_name() in [b'subjectAltName', 'subjectAltName']: + s_dns = str(s_name).split(',') + for d in s_dns: + result['dns'].append(d.split(':')[1]) + subject = x509.get_subject().get_components() + # 取主要认证名称 + if len(subject) == 1: + result['subject'] = subject[0][1].decode() else: - data['issuer'] = tmp[3].split('O=')[-1].split(',')[0] - if data['issuer'].find('/') != -1: data['issuer'] = data['issuer'].split('/')[0] - result = public.ExecShell(openssl + " x509 -in "+get.certPath+" -noout -text|grep DNS") - data['dns'] = result[0].replace('DNS:','').replace(' ','').strip().split(',') - return data + if not result['dns']: + for sub in subject: + if sub[0] == b'CN': + result['subject'] = sub[1].decode() + break + result['dns'].append(result['subject']) + else: + result['subject'] = result['dns'][0] + return result except: - print(public.get_error_info()) return None + + # 转换时间 + def strf_date(self, sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + #转换时间 def strfToTime(self,sdate): import time diff --git a/class/panelSearch.py b/class/panelSearch.py new file mode 100644 index 00000000..ca9ded2c --- /dev/null +++ b/class/panelSearch.py @@ -0,0 +1,267 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | version :1.0 +# +------------------------------------------------------------------- +# | Author: 梁凯强 <1249648969@qq.com> +# +------------------------------------------------------------------- +# | 快速检索 +# +-------------------------------------------------------------------- +import os,public,re +import zipfile,time,json +import db +class panelSearch: + __backup_path = '/www/server/panel/backup/panel_search/' + + def __init__(self): + if not os.path.exists(self.__backup_path): + os.makedirs(self.__backup_path) + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'panel_search_log')).count(): + csql = '''CREATE TABLE `panel_search_log` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `rtext` TEXT,`exts` TEXT,`path` TEXT,`mode` TEXT,`isword` TEXT,`iscase` TEXT,`noword` TEXT,`backup_path` TEXT,`time` TEXT)''' + public.M('sqlite_master').execute(csql,()) + + def dtchg(self, x): + try: + time_local = time.localtime(float(x)) + dt = time.strftime("%Y-%m-%d %H:%M:%S", time_local) + return dt + except: + return False + + def insert_settings(self, rtext, exts, path, mode, isword,iscase,noword,backup_path): + inser_time = self.dtchg(int(time.time())) + data = {"rtext": rtext, "exts": json.dumps(exts), "path": path, "mode": mode, + "isword": isword, "iscase": iscase,"noword":noword,"backup_path":backup_path,"time":inser_time} + return public.M('panel_search_log').insert(data) + + '''目录下所有的文件''' + def get_dir(self, path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False): + if rtext or noword: + result=[] + else: + result={} + if is_backup: + t = time.strftime('%Y%m%d%H%M%S') + back_zip = os.path.join(self.__backup_path, "%s.zip" % t) + zfile = zipfile.ZipFile(back_zip, "w", compression=zipfile.ZIP_DEFLATED) + else: + zfile=False + back_zip=False + return_data = [] + [[return_data.append(os.path.join(root, file)) for file in files] for root, dirs, files in os.walk(path)] + for i in return_data: + for i2 in exts: + i3 = i.split('.') + if i3[-1] == i2: + + temp = self.get_files_lin(i, text, mode, isword, iscase, noword,is_backup,rtext,zfile) + if temp: + if rtext or noword: + if isinstance(result,list): + result.append(temp) + else: + result[i] = temp + if is_backup: + if zfile: + zfile.close() + self.insert_settings(rtext, exts, path, mode, isword, iscase, noword, back_zip) + return True + return result + + '''获取单目录''' + def get_dir_files(self,path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False): + is_list = rtext or noword + if is_list: + result=[] + else: + result={} + if is_backup: + t = time.strftime('%Y%m%d%H%M%S') + back_zip = os.path.join(self.__backup_path, "%s.zip" % t) + zfile = zipfile.ZipFile(back_zip, "w", compression=zipfile.ZIP_DEFLATED) + else: + zfile=False + back_zip=False + list_data=[] + for root, dirs, files in os.walk(path): + list_data=files + break + for i in exts: + for i2 in list_data: + i3=i2.split('.') + if i3[-1]==i: + temp=self.get_files_lin(path+'/'+i2, text,mode,isword,iscase,noword,is_backup,rtext,zfile) + if temp: + if isinstance(result,list): + result.append(path+'/'+i2) + else: + result[path+'/'+i2]=temp + if is_backup: + if zfile: + zfile.close() + self.insert_settings(rtext, exts, path, mode, isword, iscase, noword, back_zip) + return result + ''' + 获取目录下所有的后缀文件 + ''' + def get_exts_files(self,path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_subdir=0,is_backup=0,rtext=False): + if len(exts)==0:return [] + if is_subdir==0: + return self.get_dir_files(path,exts,text,mode,isword,iscase,noword,is_backup,rtext) + elif is_subdir==1: + return self.get_dir(path,exts,text,mode,isword,iscase,noword,is_backup,rtext) + + ''' + 获取文件内的关键词 + ''' + def get_files_lin(self, files, text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False,back_zip=False): + if not os.path.exists(files):return False + if os.path.getsize(files) > 1024 * 1024 * 20: return False + #文件替换部分 + if rtext: + resutl=[] + try: + fp = open(files, 'r', encoding='UTF-8') + except: + fp = open(files, 'r') + content = fp.read() + fp.close() + if mode==2: + if iscase: + if not re.search(text, content, flags=re.I): return False + content = re.sub(text, rtext, content, flags=re.I) + else: + if not re.search(text, content): return False + content = re.sub(text, rtext, content) + else: + if iscase: + if not re.search(text, content, flags=re.I): return False + content = re.sub(text, rtext, content, flags=re.I) + else: + if content.find(text) == -1: return False + content = content.replace(text, rtext) + if is_backup and back_zip: + bf = files.strip('/') + back_zip.write(files, bf) + with open(files, 'w') as f: + f.write(content) + f.close() + return files + else: + #查找部分 + if noword: + resutl = [] + else: + resutl={} + try: + fp = open(files, 'r', encoding='UTF-8') + except: + fp = open(files, 'r') + i = 0 + try: + for line in fp: + i += 1 + if mode==1: + if iscase and not re.search(text, line, flags=re.I): + continue + elif not iscase and not re.search(text, line): + continue + else: + if iscase: + if not re.search(text, line, flags=re.I): continue + else: + if line.find(text) == -1: continue + if noword: + return files + resutl[i]=line + except: + pass + if resutl: + return resutl + return False + ''' + text 搜索内容 + exts 后缀名 参数例子 php,html + path 目录 + is_subdir 0 不包含子目录 1 包含子目录 + mode 0 为普通模式 1 为正则模式 + isword 1 全词匹配 0 默认 + iscase 1 不区分大小写 0 默认 + noword 1 不输出行信息 0 默认 + ''' + def get_search(self, args): + if 'text' not in args or not args.text: return {'error': 'Search content cannot be empty'} + if 'exts' not in args or not args.exts: return {'error': 'The suffix cannot be empty; please enter [ *.* ] to search all files'} + if 'path' not in args or not args.path or args.path == '/': return {'error': 'The directory cannot be empty or /'} + if not os.path.isdir(args.path): return {'error': 'Directory does not exist'} + text=args.text + exts=args.exts + path=args.path + mode = int(args.mode) if 'mode' in args else 0 + is_subdir = int(args.is_subdir) if 'is_subdir' in args else 0 + iscase = int(args.iscase) if 'iscase' in args else 0 + isword = int(args.isword) if 'isword' in args else 0 + noword = int(args.noword) if 'noword' in args else 0 + exts=exts.split(',') + is_tmpe_files=self.get_exts_files(path,exts,text,mode,isword,iscase,noword,is_subdir) + return is_tmpe_files + + ''' + text 搜索内容 + rtext 替换成的内容 + exts 后缀名 参数例子 php,html + path 目录 + is_subdir 0 不包含子目录 1 包含子目录 + mode 0 为普通模式 1 为正则模式 + isword 1 全词匹配 0 默认 + iscase 1 不区分大小写 0 默认 + noword 1 不输出行信息 0 默认 + ''' + def get_replace(self, args): + if 'text' not in args or not args.text: return {'error': 'Search content cannot be empty'} + if 'rtext' not in args or not args.text: return {'error': 'The content to be replaced cannot be empty'} + if 'exts' not in args or not args.exts: return {'error': 'The suffix cannot be empty; please enter [ *.* ] to search all files'} + if 'path' not in args or not args.path or args.path == '/': return {'error': 'The directory cannot be empty or /'} + if not os.path.isdir(args.path): return {'error': 'Directory does not exist'} + is_backup = int(args.isbackup) if 'isbackup' in args else 0 + text = args.text + rtext = args.rtext + exts = args.exts + path = args.path + mode = int(args.mode) if 'mode' in args else 0 + is_subdir = int(args.is_subdir) if 'is_subdir' in args else 0 + iscase = int(args.iscase) if 'iscase' in args else 0 + isword = int(args.isword) if 'isword' in args else 0 + noword = int(args.noword) if 'noword' in args else 0 + exts = exts.split(',') + is_tmpe_files = self.get_exts_files(path, exts, text, mode, isword, iscase, noword, is_subdir,is_backup,rtext) + return is_tmpe_files + + #替換日志 + def get_replace_logs(self,get): + import page + page = page.Page() + count = public.M('panel_search_log').order('id desc').count() + limit = 12 + info = {} + info['count'] = count + info['row'] = limit + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = get + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + data = {} + data['page'] = page.GetPage(info, '1,2,3,4,5,8') + data['data'] = public.M('panel_search_log').field('id,rtext,exts,path,mode,isword,iscase,noword,backup_path,time').order('id desc').limit(str(page.SHIFT) + ',' + str(page.ROW)).select() + if isinstance(data['data'],str): return public.returnMsg(False,[]) + for i in data['data']: + if not isinstance(i,dict): continue + if 'backup_path' in i : + path=i['backup_path'] + if os.path.exists(path): + i['is_path_status']=True + else: + i['is_path_status'] = False + return public.returnMsg(True, data) \ No newline at end of file diff --git a/class/panelSite.py b/class/panelSite.py index 76668a7d..03ba538b 100644 --- a/class/panelSite.py +++ b/class/panelSite.py @@ -32,7 +32,7 @@ class panelSite(panelRedirect): is_ipv6 = False def __init__(self): - self.setupPath = '/www/server' + self.setupPath = public.get_setup_path() path = self.setupPath + '/panel/vhost/nginx' if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) path = self.setupPath + '/panel/vhost/apache' @@ -43,7 +43,7 @@ class panelSite(panelRedirect): if not os.path.exists(path + '/index.html'): public.ExecShell('mkdir -p ' + path) public.ExecShell('wget -O ' + path + '/index.html ' + public.get_url() + '/stop_en.html &') - self.__proxyfile = '/www/server/panel/data/proxyfile.json' + self.__proxyfile = '{}/data/proxyfile.json'.format(public.get_panel_path()) self.OldConfigFile() if os.path.exists(self.nginx_conf_bak): os.remove(self.nginx_conf_bak) if os.path.exists(self.apache_conf_bak): os.remove(self.apache_conf_bak) @@ -84,6 +84,7 @@ class panelSite(panelRedirect): # 添加apache端口 def apacheAddPort(self, port): + port = str(port) filename = self.setupPath + '/apache/conf/extra/httpd-ssl.conf' if os.path.exists(filename): ssl_conf = public.readFile(filename) @@ -1747,6 +1748,10 @@ listener SSL443 { # Nginx配置 file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + + # Node项目 + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + ng_file = file conf = public.readFile(file) @@ -1793,6 +1798,7 @@ listener SSL443 { # Apache配置 file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' conf = public.readFile(file) ap_static_security = self._get_ap_static_security(conf) if conf: @@ -1907,8 +1913,10 @@ listener SSL443 { # HttpToHttps def HttpToHttps(self, get): siteName = get.siteName - # Nginx配置 - file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + #Nginx配置 + file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_'+siteName+'.conf' conf = public.readFile(file) if conf: if conf.find('ssl_certificate') == -1: return public.returnMsg(False, 'NOT_OPEN_SSL') @@ -1918,10 +1926,12 @@ listener SSL443 { rewrite ^(/.*)$ https://$host$1 permanent; } #HTTP_TO_HTTPS_END""" - conf = conf.replace('#error_page 404/404.html;', to) - public.writeFile(file, conf) + conf = conf.replace('#error_page 404/404.html;',to) + public.writeFile(file,conf) - file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + file = self.setupPath + '/panel/vhost/apache/'+siteName+'.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/apache/node_'+siteName+'.conf' conf = public.readFile(file) if conf: httpTohttos = '''combined @@ -1979,6 +1989,9 @@ listener SSL443 { # 是否跳转到https def IsToHttps(self, siteName): file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_'+siteName+'.conf' + if not os.path.exists(file): return False conf = public.readFile(file) if conf: if conf.find('HTTP_TO_HTTPS_START') != -1: return True @@ -1990,6 +2003,8 @@ listener SSL443 { siteName = get.siteName file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' conf = public.readFile(file) if conf: rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" @@ -2031,6 +2046,8 @@ listener SSL443 { public.writeFile(file, conf) file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' conf = public.readFile(file) if conf: rep = "\n(.|\n)*<\/VirtualHost>" @@ -2090,6 +2107,9 @@ listener SSL443 { key = public.readFile(keypath) csr = public.readFile(csrpath) file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf' + + # 是否为node项目 + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/node_' + siteName + '.conf' if public.get_webserver() == "openlitespeed": file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/detail/' + siteName + '.conf' conf = public.readFile(file) @@ -2393,11 +2413,16 @@ listener SSL443 { domains = '' id = public.M('sites').where("name=?", (siteName,)).getField('id') tmp = public.M('domain').where("pid=?", (id,)).field('name').select() + node = public.M('sites').where('id=? and project_type=?', (id, 'Node')).count() + if node: + node = 'node_' + else: + node = '' for key in tmp: domains += key['name'] + ',' try: - if public.get_webserver() == 'nginx': - conf = public.readFile(self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf') + if (public.get_webserver() == 'nginx'): + conf = public.readFile(self.setupPath + '/panel/vhost/nginx/' + node + siteName + '.conf') if conf.find('301-START') == -1: result['domain'] = domains[:-1] result['src'] = "" @@ -2411,7 +2436,7 @@ listener SSL443 { src = '' if tmp: src = tmp.groups()[0] elif public.get_webserver() == 'apache': - conf = public.readFile(self.setupPath + '/panel/vhost/apache/' + siteName + '.conf') + conf = public.readFile(self.setupPath + '/panel/vhost/apache/' + node + siteName + '.conf') if conf.find('301-START') == -1: result['domain'] = domains[:-1] result['src'] = "" @@ -3122,6 +3147,7 @@ server public.WriteLog("TYPE_SITE", "SITE_PHPVERSION_SUCCESS", (siteName, version)) return public.returnMsg(True, 'SITE_PHPVERSION_SUCCESS', (siteName, version)) except: + return public.get_error_info() return public.returnMsg(False, 'PHP_SETUP_FAILED') # 是否开启目录防御 @@ -3677,6 +3703,15 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] if re.search(rep, data): return True + def old_proxy_conf(self,conf,ng_conf_file,get): + rep = 'location\s*\~\*.*gif\|png\|jpg\|css\|js\|woff\|woff2\)\$' + if not re.search(rep,conf): + return conf + + self.RemoveProxy(get) + self.CreateProxy(get) + return public.readFile(ng_conf_file) + # 修改反向代理 def ModifyProxy(self, get): proxyname_md5 = self.__calc_md5(get.proxyname) @@ -3689,6 +3724,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] if self.__CheckStart(get): return self.__CheckStart(get) conf = self.__read_config(self.__proxyfile) + random_string = public.GetRandomString(8) for i in range(len(conf)): if conf[i]["proxyname"] == get.proxyname and conf[i]["sitename"] == get.sitename: if int(get.type) != 1: @@ -3705,6 +3741,7 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] public.ExecShell("mv {f}_bak {f}".format(f=ng_conf_file)) public.ExecShell("mv {f}_bak {f}".format(f=ols_conf_file)) ng_conf = public.readFile(ng_conf_file) + ng_conf = self.old_proxy_conf(ng_conf, ng_conf_file, get) # 修改nginx配置 # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri php_pass_proxy = get.proxysite @@ -3730,13 +3767,22 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] ng_conf = re.sub(cache_rep, "proxy_cache_valid 200 304 301 302 {0}m;".format(get.cachetime), ng_conf) else: + # ng_cache = """ + # proxy_ignore_headers Set-Cookie Cache-Control expires; + # proxy_cache cache_one; + # proxy_cache_key $host$uri$is_args$args; + # proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) ng_cache = """ + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + expires 12h; + } proxy_ignore_headers Set-Cookie Cache-Control expires; proxy_cache cache_one; proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) if self.check_annotate(ng_conf): - cache_rep = '\n\s*#Set\s*Nginx\s*Cache(.|\n)*no-cache;' + cache_rep = '\n\s*#Set\s*Nginx\s*Cache(.|\n)*no-cache;\s*\n*\s*\}' ng_conf = re.sub(cache_rep, '\n\t#Set Nginx Cache\n' + ng_cache, ng_conf) else: # cache_rep = '#proxy_set_header\s+Connection\s+"upgrade";' @@ -3745,16 +3791,30 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] r"\n\tproxy_set_header\s+REMOTE-HOST\s+\$remote_addr;\n\t#Set Nginx Cache" + ng_cache, ng_conf) else: + no_cache = """ + #Set Nginx Cache + set $static_file%s 0; + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + set $static_file%s 1; + expires 12h; + } + if ( $static_file%s = 0 ) + { + add_header Cache-Control no-cache; + }""" % (random_string, random_string, random_string) if self.check_annotate(ng_conf): rep = r'\n\s*#Set\s*Nginx\s*Cache(.|\n)*\d+m;' - ng_conf = re.sub(rep, - "\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", - ng_conf) + # ng_conf = re.sub(rep, + # "\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", + # ng_conf) + ng_conf = re.sub(rep,no_cache,ng_conf) else: rep = r"\s+proxy_cache\s+cache_one.*[\n\s\w\_\";\$]+m;" - ng_conf = re.sub(rep, - r"\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", - ng_conf) + # ng_conf = re.sub(rep, + # r"\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", + # ng_conf) + ng_conf = re.sub(rep,no_cache,ng_conf) sub_rep = "sub_filter" subfilter = json.loads(get.subfilter) @@ -3841,18 +3901,36 @@ RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] type = int(get.type) cache = int(get.cache) cachetime = int(get.cachetime) + proxysite = get.proxysite + proxydir = get.proxydir ng_file = self.setupPath + "/panel/vhost/nginx/" + sitename + ".conf" ap_file = self.setupPath + "/panel/vhost/apache/" + sitename + ".conf" p_conf = self.__read_config(self.__proxyfile) + random_string = public.GetRandomString(8) # 配置Nginx # 构造清理缓存连接 # 构造缓存配置 ng_cache = """ + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + expires 12h; + } proxy_ignore_headers Set-Cookie Cache-Control expires; proxy_cache cache_one; proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 304 301 302 %sm;""" % (cachetime) + no_cache = """ + set $static_file%s 0; + if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) + { + set $static_file%s 1; + expires 12h; + } + if ( $static_file%s = 0 ) + { + add_header Cache-Control no-cache; + }""" % (random_string,random_string,random_string) # rep = "(https?://[\w\.]+)" # proxysite1 = re.search(rep,get.proxysite).group(1) ng_proxy = ''' @@ -3903,28 +3981,32 @@ location ^~ %s ng_sub_filter = '' # 构造反向代理 # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri - php_pass_proxy = get.proxysite - if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: - php_pass_proxy = re.search('(https?\:\/\/[\w\.]+)', get.proxysite).group(0) + # php_pass_proxy = get.proxysite + # if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: + # php_pass_proxy = re.search('(https?\:\/\/[\w\.]+)', get.proxysite).group(0) if advanced == 1: + if proxydir[-1] != '/': + proxydir = '{}/'.format(proxydir) + if proxysite[-1] != '/': + proxysite = '{}/'.format(proxysite) if type == 1 and cache == 1: ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy, get.todomain, get.proxydir, get.proxysite, get.todomain, + proxydir, proxydir, proxysite, get.todomain, public.GetMsg("NGINX_PERSISTENCE"), ng_sub_filter, ng_cache, get.proxydir) if type == 1 and cache == 0: ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy, get.todomain, get.proxydir, get.proxysite, get.todomain, - public.GetMsg("NGINX_PERSISTENCE"), ng_sub_filter, '\tadd_header Cache-Control no-cache;', + get.proxydir, get.proxydir, proxysite, get.todomain, + public.GetMsg("NGINX_PERSISTENCE"), ng_sub_filter, no_cache, get.proxydir) else: if type == 1 and cache == 1: ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy, get.todomain, get.proxydir, get.proxysite, get.todomain, + get.proxydir, get.proxydir, get.proxysite, get.todomain, public.GetMsg("NGINX_PERSISTENCE"), ng_sub_filter, ng_cache, get.proxydir) if type == 1 and cache == 0: ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy, get.todomain, get.proxydir, get.proxysite, get.todomain, - public.GetMsg("NGINX_PERSISTENCE"), ng_sub_filter, '\tadd_header Cache-Control no-cache;', + get.proxydir, get.proxydir, get.proxysite, get.todomain, + public.GetMsg("NGINX_PERSISTENCE"), ng_sub_filter, no_cache, get.proxydir) public.writeFile(ng_proxyfile, ng_proxy_cache) @@ -4007,6 +4089,8 @@ location ^~ %s # 取伪静态规则应用列表 def GetRewriteList(self, get): + if get.siteName.find('node_') == 0: + get.siteName = get.siteName.replace('node_', '') rewriteList = {} ws = public.get_webserver() if ws == "openlitespeed": @@ -4250,15 +4334,16 @@ location ^~ %s rep = "\n#errorlog(.|\n)*compressArchive\s*1\s*\n#}" tmp = re.search(rep, conf) tmp = tmp.group() - result = '' - if s == 'on': - for l in tmp.strip().splitlines(): - result += "\n#" + l - else: - for l in tmp.splitlines(): - result += "\n" + l[1:] - conf = re.sub(rep, "\n" + result.strip(), conf) - public.writeFile(filename, conf) + if tmp: + result = '' + if s == 'on': + for l in tmp.strip().splitlines(): + result += "\n#"+l + else: + for l in tmp.splitlines(): + result += "\n"+l[1:] + conf = re.sub(rep,"\n"+result.strip(),conf) + public.writeFile(filename,conf) public.serviceReload() return public.returnMsg(True, 'SUCCESS') @@ -4488,6 +4573,7 @@ location ^~ %s def GetSiteRunPath(self, get): siteName = public.M('sites').where('id=?', (get.id,)).getField('name') sitePath = public.M('sites').where('id=?', (get.id,)).getField('path') + if not siteName: return {"runPath":"/",'dirs':[]} path = sitePath if public.get_webserver() == 'nginx': filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' @@ -5000,3 +5086,95 @@ RewriteRule \.(BTPFILE)$ /404.html [R,NC] def modify_dir_auth_pass(self, get): sd = site_dir_auth.SiteDirAuth() return sd.modify_dir_auth_pass(get) + + def _check_path_total(self,path, limit): + """ + 根据路径获取文件/目录大小 + @path 文件或者目录路径 + return int + """ + + if not os.path.exists(path): return 0; + if not os.path.isdir(path): return os.path.getsize(path) + size_total = 0 + for nf in os.walk(path): + for f in nf[2]: + filename = nf[0] + '/' + f + if not os.path.exists(filename): continue; + if os.path.islink(filename): continue; + size_total += os.path.getsize(filename) + if size_total >= limit: return limit + return size_total + + def get_average_num(self,slist): + """ + @获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size + print(slist) + if len(slist) > 1: + slist = sorted(slist) + limit_size =int((slist[0] + slist[-1])/2 * 0.85) + return limit_size + + + def check_del_data(self,get): + """ + @删除前置检测 + @ids = [1,2,3] + """ + ids = json.loads(get['ids']) + slist = {} + result = [] + + import database + db_data = database.database().get_database_size(None) + + limit_size = 50 * 1024 * 1024 + f_list_size = [];db_list_size = [] + for id in ids: + data = public.M('sites').where("id=?",(id,)).field('id,name,path,addtime').find(); + if not data: continue + + addtime = public.to_date(times = data['addtime']) + + data['st_time'] = addtime + data['limit'] = False + data['backup_count'] = public.M('backup').where("pid=? AND type=?",(data['id'],'0')).count() + f_size = self._check_path_total(data['path'],limit_size) + data['total'] = f_size; + data['score'] = 0 + + #目录太小不计分 + if f_size > 0: + f_list_size.append(f_size) + + # 10k 目录不参与排序 + if f_size > 10 * 1024: data['score'] = int(time.time() - addtime) + f_size + + if data['total'] >= limit_size: data['limit'] = True + data['database'] = False + + find = public.M('databases').field('id,pid,name,ps,addtime').where('pid=?',(data['id'],)).find() + if find: + db_addtime = public.to_date(times = find['addtime']) + + data['database'] = db_data[find['name']] + data['database']['st_time'] = db_addtime + + db_score = 0; + db_size = data['database']['total'] + + if db_size > 0: + db_list_size.append(db_size) + if db_size > 50 * 1024: db_score += int(time.time() - db_addtime) + db_size + + data['score'] += db_score + result.append(data) + + slist['data'] = sorted(result,key= lambda x:x['score'],reverse=True) + slist['file_size'] = self.get_average_num(f_list_size) + slist['db_size'] = self.get_average_num(db_list_size) + return slist \ No newline at end of file diff --git a/class/panelSiteController.py b/class/panelSiteController.py new file mode 100644 index 00000000..ef725f00 --- /dev/null +++ b/class/panelSiteController.py @@ -0,0 +1,112 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# 宝塔Linux面板 +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# 网站管理控制器 +#------------------------------ +import os,sys,public + +class panelSiteController: + + + def __init__(self): + pass + + + + def get_parser_list(self,args): + ''' + @name 获取支持的解释器列表 + @author hwliang<2021-07-13> + @param args + @return list + ''' + return public.return_data(True,public.read_config('parser')) + + + def get_parser_versions(self,args): + ''' + @name 获取指定解释器可用版本列表 + @author hwliang<2021-07-13> + @param args{ + parser_name: string<解释器名称> + } + @return list + ''' + try: + public.exists_args('parser_name',args) + except Exception as ex: + return public.return_data(False,None,1001,ex) + parser_name = args.parser_name.strip() + config_data = public.read_config('parser') + versions = [] + result = public.return_data(False,versions) + for parser_data in config_data: + if parser_data['name'] != parser_name: continue + if not parser_data['show']: return result + if not parser_data['versions']: return result + for version in parser_data['versions']: + if isinstance(version['check'],str): + version['check'] = [version['check']] + for check in version['check']: + if os.path.exists(check): + versions.append(version) + return public.return_data(True,versions) + + + + + + + + def create_site(self,args): + ''' + @name 创建网站 + @author hwliang<2021-07-13> + @param args { + data: { + siteName: string<网站名称>, + domains: list<域名列表>, // 如:["www.bt.cn:80","bt.cn:80"] + parser_type: string<解释器类型>, // 从 get_parser_list 接口中获取 + parser_version: string<解释器版本>, // 从 get_parser_versions 接口中获取 + ps: string<网站备注>, + type_id: int<分类标识>, + path: string<网站根目录>, + stream_info: { // TCP、UDP时传入 + is_stream: bool<是否为stream>, + pool: string<协议类型TCP/UDP>, + dst_address: string<目标地址>, + dst_port: int<目标端口>, + local_port: int<本地映射端口> + }, + process_info: { //绑定进程时传入 + is_process: bool<是否为启动指定文件>, + cwd: string<运行目录>, + run_file: string<启动文件>, + run_args: string<启动参数>, + run_cmd: string<启动命令> //与 run_file/run_args 互斥 + env: list<环境变量> + }, + ftp_info: { //需要同时创建FTP时传入 + create: bool<是否创建>, + username: string<用户名>, + password: string<密码>, + path: string<根目录> + }, + database_info: { //需要同时创建数据库时传入 + create: bool<是否创建>, + username: string<用户名>, + password: string<密码>, + db_name: string<数据库名>, + codeing: string<字符集> + } + } + } + ''' + diff --git a/class/panelTask.py b/class/panelTask.py index 69d4cbd5..f2332ef2 100644 --- a/class/panelTask.py +++ b/class/panelTask.py @@ -87,11 +87,11 @@ class bt_task: # 创建任务 def create_task(self, task_name, task_type, task_shell, other=''): self.clean_log() - public.M(self.__table).add('name,type,shell,other,addtime,status', + task_id = public.M(self.__table).add('name,type,shell,other,addtime,status', (task_name, task_type, task_shell, other, int(time.time()), 0)) public.WriteFile(self.__task_tips, 'True') public.ExecShell("/etc/init.d/bt start") - return True + return task_id # 修改任务 def modify_task(self, id, key, value): @@ -215,6 +215,20 @@ class bt_task: except: print(public.get_error_info()) + # 前端通过任务ID取某一个任务的日志 + def get_task_log_by_id(self, get): + task_id = get.id + task_type = get.task_type + log_data = {} + if "num" in get: + num = int(get.num) + log_data = self.get_task_log(task_id, task_type, num) + else: + log_data = self.get_task_log(task_id, task_type) + task_obj = self.get_task_find(task_id) + log_data["status"] = task_obj["status"] + return log_data + # 取任务执行日志 def get_task_log(self, id, task_type, num=5): log_file = self.__task_path + str(id) + '.log' diff --git a/class/panel_telegram_bot.py b/class/panel_telegram_bot.py new file mode 100644 index 00000000..adb2052d --- /dev/null +++ b/class/panel_telegram_bot.py @@ -0,0 +1,68 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | 宝塔Linux面板 x3 +# +------------------------------------------------------------------- +# | Copyright (c) 2015-2017 宝塔软件(http://bt.cn) All rights reserved. +# +------------------------------------------------------------------- +# | Author: zhw +# +------------------------------------------------------------------- + +import public +import json +import os + +py_bin = public.get_python_bin() +pip = public.get_pip_bin() + +try: + import telegram +except: + public.ExecShell('{} install telegram'.format(pip)) + import telegram + +class panel_telegram_bot: + panel_path = public.get_panel_path() + __tg_conf_file = '{}/data/tg_bot.json'.format(panel_path) + + # 设置tg机器人 + def set_tg_bot(self,get): + """ + bot_token:12345677:CCCCCCCC-a0VUo2jjrCCfffaaaaCCDDD + my_id:1234567890 + """ + data = {"setup":True,"bot_token":get.bot_token,"my_id":get.my_id} + public.writeFile(self.__tg_conf_file,json.dumps(data)) + return public.returnMsg(True,"Setup successfully") + + # 删除tg机器人 + def del_tg_bot(self,get): + if os.path.exists(self.__tg_conf_file): + os.remove(self.__tg_conf_file) + return public.returnMsg(True, "Remove successfully") + + # 获取tg机器人信息 + def get_tg_conf(self,get=None): + conf = public.readFile(self.__tg_conf_file) + if not conf: + return {"setup":False,"bot_token":"","my_id":""} + try: + return json.loads(conf) + except: + return {"setup":False,"bot_token":"","my_id":""} + + def process_character(self,content): + character = ['.',',','!',':','%','[',']','\/','_','-','>'] + for c in character: + if c in content and '\\{}'.format(c) not in content: + content = content.replace(c,'\\'+c) + return content + + + # 使用tg机器人发送消息 + def send_by_tg_bot(self,content,parse_mode=None): + "parse_mode 消息格式 html/markdown/markdownv2" + content = self.process_character(content) + conf = self.get_tg_conf() + bot = telegram.Bot(conf['bot_token']) + result = bot.send_message(text=content, chat_id=int(conf['my_id']), parse_mode="MarkdownV2") + return result \ No newline at end of file diff --git a/class/pexpect/ANSI.py b/class/pexpect/ANSI.py new file mode 100644 index 00000000..1cd2e90e --- /dev/null +++ b/class/pexpect/ANSI.py @@ -0,0 +1,351 @@ +'''This implements an ANSI (VT100) terminal emulator as a subclass of screen. + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +# references: +# http://en.wikipedia.org/wiki/ANSI_escape_code +# http://www.retards.org/terminals/vt102.html +# http://vt100.net/docs/vt102-ug/contents.html +# http://vt100.net/docs/vt220-rm/ +# http://www.termsys.demon.co.uk/vtansi.htm + +from . import screen +from . import FSM +import string + +# +# The 'Do.*' functions are helper functions for the ANSI class. +# +def DoEmit (fsm): + + screen = fsm.memory[0] + screen.write_ch(fsm.input_symbol) + +def DoStartNumber (fsm): + + fsm.memory.append (fsm.input_symbol) + +def DoBuildNumber (fsm): + + ns = fsm.memory.pop() + ns = ns + fsm.input_symbol + fsm.memory.append (ns) + +def DoBackOne (fsm): + + screen = fsm.memory[0] + screen.cursor_back () + +def DoBack (fsm): + + count = int(fsm.memory.pop()) + screen = fsm.memory[0] + screen.cursor_back (count) + +def DoDownOne (fsm): + + screen = fsm.memory[0] + screen.cursor_down () + +def DoDown (fsm): + + count = int(fsm.memory.pop()) + screen = fsm.memory[0] + screen.cursor_down (count) + +def DoForwardOne (fsm): + + screen = fsm.memory[0] + screen.cursor_forward () + +def DoForward (fsm): + + count = int(fsm.memory.pop()) + screen = fsm.memory[0] + screen.cursor_forward (count) + +def DoUpReverse (fsm): + + screen = fsm.memory[0] + screen.cursor_up_reverse() + +def DoUpOne (fsm): + + screen = fsm.memory[0] + screen.cursor_up () + +def DoUp (fsm): + + count = int(fsm.memory.pop()) + screen = fsm.memory[0] + screen.cursor_up (count) + +def DoHome (fsm): + + c = int(fsm.memory.pop()) + r = int(fsm.memory.pop()) + screen = fsm.memory[0] + screen.cursor_home (r,c) + +def DoHomeOrigin (fsm): + + c = 1 + r = 1 + screen = fsm.memory[0] + screen.cursor_home (r,c) + +def DoEraseDown (fsm): + + screen = fsm.memory[0] + screen.erase_down() + +def DoErase (fsm): + + arg = int(fsm.memory.pop()) + screen = fsm.memory[0] + if arg == 0: + screen.erase_down() + elif arg == 1: + screen.erase_up() + elif arg == 2: + screen.erase_screen() + +def DoEraseEndOfLine (fsm): + + screen = fsm.memory[0] + screen.erase_end_of_line() + +def DoEraseLine (fsm): + + arg = int(fsm.memory.pop()) + screen = fsm.memory[0] + if arg == 0: + screen.erase_end_of_line() + elif arg == 1: + screen.erase_start_of_line() + elif arg == 2: + screen.erase_line() + +def DoEnableScroll (fsm): + + screen = fsm.memory[0] + screen.scroll_screen() + +def DoCursorSave (fsm): + + screen = fsm.memory[0] + screen.cursor_save_attrs() + +def DoCursorRestore (fsm): + + screen = fsm.memory[0] + screen.cursor_restore_attrs() + +def DoScrollRegion (fsm): + + screen = fsm.memory[0] + r2 = int(fsm.memory.pop()) + r1 = int(fsm.memory.pop()) + screen.scroll_screen_rows (r1,r2) + +def DoMode (fsm): + + screen = fsm.memory[0] + mode = fsm.memory.pop() # Should be 4 + # screen.setReplaceMode () + +def DoLog (fsm): + + screen = fsm.memory[0] + fsm.memory = [screen] + fout = open ('log', 'a') + fout.write (fsm.input_symbol + ',' + fsm.current_state + '\n') + fout.close() + +class term (screen.screen): + + '''This class is an abstract, generic terminal. + This does nothing. This is a placeholder that + provides a common base class for other terminals + such as an ANSI terminal. ''' + + def __init__ (self, r=24, c=80, *args, **kwargs): + + screen.screen.__init__(self, r,c,*args,**kwargs) + +class ANSI (term): + '''This class implements an ANSI (VT100) terminal. + It is a stream filter that recognizes ANSI terminal + escape sequences and maintains the state of a screen object. ''' + + def __init__ (self, r=24,c=80,*args,**kwargs): + + term.__init__(self,r,c,*args,**kwargs) + + #self.screen = screen (24,80) + self.state = FSM.FSM ('INIT',[self]) + self.state.set_default_transition (DoLog, 'INIT') + self.state.add_transition_any ('INIT', DoEmit, 'INIT') + self.state.add_transition ('\x1b', 'INIT', None, 'ESC') + self.state.add_transition_any ('ESC', DoLog, 'INIT') + self.state.add_transition ('(', 'ESC', None, 'G0SCS') + self.state.add_transition (')', 'ESC', None, 'G1SCS') + self.state.add_transition_list ('AB012', 'G0SCS', None, 'INIT') + self.state.add_transition_list ('AB012', 'G1SCS', None, 'INIT') + self.state.add_transition ('7', 'ESC', DoCursorSave, 'INIT') + self.state.add_transition ('8', 'ESC', DoCursorRestore, 'INIT') + self.state.add_transition ('M', 'ESC', DoUpReverse, 'INIT') + self.state.add_transition ('>', 'ESC', DoUpReverse, 'INIT') + self.state.add_transition ('<', 'ESC', DoUpReverse, 'INIT') + self.state.add_transition ('=', 'ESC', None, 'INIT') # Selects application keypad. + self.state.add_transition ('#', 'ESC', None, 'GRAPHICS_POUND') + self.state.add_transition_any ('GRAPHICS_POUND', None, 'INIT') + self.state.add_transition ('[', 'ESC', None, 'ELB') + # ELB means Escape Left Bracket. That is ^[[ + self.state.add_transition ('H', 'ELB', DoHomeOrigin, 'INIT') + self.state.add_transition ('D', 'ELB', DoBackOne, 'INIT') + self.state.add_transition ('B', 'ELB', DoDownOne, 'INIT') + self.state.add_transition ('C', 'ELB', DoForwardOne, 'INIT') + self.state.add_transition ('A', 'ELB', DoUpOne, 'INIT') + self.state.add_transition ('J', 'ELB', DoEraseDown, 'INIT') + self.state.add_transition ('K', 'ELB', DoEraseEndOfLine, 'INIT') + self.state.add_transition ('r', 'ELB', DoEnableScroll, 'INIT') + self.state.add_transition ('m', 'ELB', self.do_sgr, 'INIT') + self.state.add_transition ('?', 'ELB', None, 'MODECRAP') + self.state.add_transition_list (string.digits, 'ELB', DoStartNumber, 'NUMBER_1') + self.state.add_transition_list (string.digits, 'NUMBER_1', DoBuildNumber, 'NUMBER_1') + self.state.add_transition ('D', 'NUMBER_1', DoBack, 'INIT') + self.state.add_transition ('B', 'NUMBER_1', DoDown, 'INIT') + self.state.add_transition ('C', 'NUMBER_1', DoForward, 'INIT') + self.state.add_transition ('A', 'NUMBER_1', DoUp, 'INIT') + self.state.add_transition ('J', 'NUMBER_1', DoErase, 'INIT') + self.state.add_transition ('K', 'NUMBER_1', DoEraseLine, 'INIT') + self.state.add_transition ('l', 'NUMBER_1', DoMode, 'INIT') + ### It gets worse... the 'm' code can have infinite number of + ### number;number;number before it. I've never seen more than two, + ### but the specs say it's allowed. crap! + self.state.add_transition ('m', 'NUMBER_1', self.do_sgr, 'INIT') + ### LED control. Same implementation problem as 'm' code. + self.state.add_transition ('q', 'NUMBER_1', self.do_decsca, 'INIT') + + # \E[?47h switch to alternate screen + # \E[?47l restores to normal screen from alternate screen. + self.state.add_transition_list (string.digits, 'MODECRAP', DoStartNumber, 'MODECRAP_NUM') + self.state.add_transition_list (string.digits, 'MODECRAP_NUM', DoBuildNumber, 'MODECRAP_NUM') + self.state.add_transition ('l', 'MODECRAP_NUM', self.do_modecrap, 'INIT') + self.state.add_transition ('h', 'MODECRAP_NUM', self.do_modecrap, 'INIT') + +#RM Reset Mode Esc [ Ps l none + self.state.add_transition (';', 'NUMBER_1', None, 'SEMICOLON') + self.state.add_transition_any ('SEMICOLON', DoLog, 'INIT') + self.state.add_transition_list (string.digits, 'SEMICOLON', DoStartNumber, 'NUMBER_2') + self.state.add_transition_list (string.digits, 'NUMBER_2', DoBuildNumber, 'NUMBER_2') + self.state.add_transition_any ('NUMBER_2', DoLog, 'INIT') + self.state.add_transition ('H', 'NUMBER_2', DoHome, 'INIT') + self.state.add_transition ('f', 'NUMBER_2', DoHome, 'INIT') + self.state.add_transition ('r', 'NUMBER_2', DoScrollRegion, 'INIT') + ### It gets worse... the 'm' code can have infinite number of + ### number;number;number before it. I've never seen more than two, + ### but the specs say it's allowed. crap! + self.state.add_transition ('m', 'NUMBER_2', self.do_sgr, 'INIT') + ### LED control. Same problem as 'm' code. + self.state.add_transition ('q', 'NUMBER_2', self.do_decsca, 'INIT') + self.state.add_transition (';', 'NUMBER_2', None, 'SEMICOLON_X') + + # Create a state for 'q' and 'm' which allows an infinite number of ignored numbers + self.state.add_transition_any ('SEMICOLON_X', DoLog, 'INIT') + self.state.add_transition_list (string.digits, 'SEMICOLON_X', DoStartNumber, 'NUMBER_X') + self.state.add_transition_list (string.digits, 'NUMBER_X', DoBuildNumber, 'NUMBER_X') + self.state.add_transition_any ('NUMBER_X', DoLog, 'INIT') + self.state.add_transition ('m', 'NUMBER_X', self.do_sgr, 'INIT') + self.state.add_transition ('q', 'NUMBER_X', self.do_decsca, 'INIT') + self.state.add_transition (';', 'NUMBER_X', None, 'SEMICOLON_X') + + def process (self, c): + """Process a single character. Called by :meth:`write`.""" + if isinstance(c, bytes): + c = self._decode(c) + self.state.process(c) + + def process_list (self, l): + + self.write(l) + + def write (self, s): + """Process text, writing it to the virtual screen while handling + ANSI escape codes. + """ + if isinstance(s, bytes): + s = self._decode(s) + for c in s: + self.process(c) + + def flush (self): + pass + + def write_ch (self, ch): + '''This puts a character at the current cursor position. The cursor + position is moved forward with wrap-around, but no scrolling is done if + the cursor hits the lower-right corner of the screen. ''' + + if isinstance(ch, bytes): + ch = self._decode(ch) + + #\r and \n both produce a call to cr() and lf(), respectively. + ch = ch[0] + + if ch == u'\r': + self.cr() + return + if ch == u'\n': + self.crlf() + return + if ch == chr(screen.BS): + self.cursor_back() + return + self.put_abs(self.cur_r, self.cur_c, ch) + old_r = self.cur_r + old_c = self.cur_c + self.cursor_forward() + if old_c == self.cur_c: + self.cursor_down() + if old_r != self.cur_r: + self.cursor_home (self.cur_r, 1) + else: + self.scroll_up () + self.cursor_home (self.cur_r, 1) + self.erase_line() + + def do_sgr (self, fsm): + '''Select Graphic Rendition, e.g. color. ''' + screen = fsm.memory[0] + fsm.memory = [screen] + + def do_decsca (self, fsm): + '''Select character protection attribute. ''' + screen = fsm.memory[0] + fsm.memory = [screen] + + def do_modecrap (self, fsm): + '''Handler for \x1b[?h and \x1b[?l. If anyone + wanted to actually use these, they'd need to add more states to the + FSM rather than just improve or override this method. ''' + screen = fsm.memory[0] + fsm.memory = [screen] diff --git a/class/pexpect/FSM.py b/class/pexpect/FSM.py new file mode 100644 index 00000000..46b392ea --- /dev/null +++ b/class/pexpect/FSM.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python + +'''This module implements a Finite State Machine (FSM). In addition to state +this FSM also maintains a user defined "memory". So this FSM can be used as a +Push-down Automata (PDA) since a PDA is a FSM + memory. + +The following describes how the FSM works, but you will probably also need to +see the example function to understand how the FSM is used in practice. + +You define an FSM by building tables of transitions. For a given input symbol +the process() method uses these tables to decide what action to call and what +the next state will be. The FSM has a table of transitions that associate: + + (input_symbol, current_state) --> (action, next_state) + +Where "action" is a function you define. The symbols and states can be any +objects. You use the add_transition() and add_transition_list() methods to add +to the transition table. The FSM also has a table of transitions that +associate: + + (current_state) --> (action, next_state) + +You use the add_transition_any() method to add to this transition table. The +FSM also has one default transition that is not associated with any specific +input_symbol or state. You use the set_default_transition() method to set the +default transition. + +When an action function is called it is passed a reference to the FSM. The +action function may then access attributes of the FSM such as input_symbol, +current_state, or "memory". The "memory" attribute can be any object that you +want to pass along to the action functions. It is not used by the FSM itself. +For parsing you would typically pass a list to be used as a stack. + +The processing sequence is as follows. The process() method is given an +input_symbol to process. The FSM will search the table of transitions that +associate: + + (input_symbol, current_state) --> (action, next_state) + +If the pair (input_symbol, current_state) is found then process() will call the +associated action function and then set the current state to the next_state. + +If the FSM cannot find a match for (input_symbol, current_state) it will then +search the table of transitions that associate: + + (current_state) --> (action, next_state) + +If the current_state is found then the process() method will call the +associated action function and then set the current state to the next_state. +Notice that this table lacks an input_symbol. It lets you define transitions +for a current_state and ANY input_symbol. Hence, it is called the "any" table. +Remember, it is always checked after first searching the table for a specific +(input_symbol, current_state). + +For the case where the FSM did not match either of the previous two cases the +FSM will try to use the default transition. If the default transition is +defined then the process() method will call the associated action function and +then set the current state to the next_state. This lets you define a default +transition as a catch-all case. You can think of it as an exception handler. +There can be only one default transition. + +Finally, if none of the previous cases are defined for an input_symbol and +current_state then the FSM will raise an exception. This may be desirable, but +you can always prevent this just by defining a default transition. + +Noah Spurrier 20020822 + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +class ExceptionFSM(Exception): + + '''This is the FSM Exception class.''' + + def __init__(self, value): + self.value = value + + def __str__(self): + return 'ExceptionFSM: ' + str(self.value) + +class FSM: + + '''This is a Finite State Machine (FSM). + ''' + + def __init__(self, initial_state, memory=None): + + '''This creates the FSM. You set the initial state here. The "memory" + attribute is any object that you want to pass along to the action + functions. It is not used by the FSM. For parsing you would typically + pass a list to be used as a stack. ''' + + # Map (input_symbol, current_state) --> (action, next_state). + self.state_transitions = {} + # Map (current_state) --> (action, next_state). + self.state_transitions_any = {} + self.default_transition = None + + self.input_symbol = None + self.initial_state = initial_state + self.current_state = self.initial_state + self.next_state = None + self.action = None + self.memory = memory + + def reset (self): + + '''This sets the current_state to the initial_state and sets + input_symbol to None. The initial state was set by the constructor + __init__(). ''' + + self.current_state = self.initial_state + self.input_symbol = None + + def add_transition (self, input_symbol, state, action=None, next_state=None): + + '''This adds a transition that associates: + + (input_symbol, current_state) --> (action, next_state) + + The action may be set to None in which case the process() method will + ignore the action and only set the next_state. The next_state may be + set to None in which case the current state will be unchanged. + + You can also set transitions for a list of symbols by using + add_transition_list(). ''' + + if next_state is None: + next_state = state + self.state_transitions[(input_symbol, state)] = (action, next_state) + + def add_transition_list (self, list_input_symbols, state, action=None, next_state=None): + + '''This adds the same transition for a list of input symbols. + You can pass a list or a string. Note that it is handy to use + string.digits, string.whitespace, string.letters, etc. to add + transitions that match character classes. + + The action may be set to None in which case the process() method will + ignore the action and only set the next_state. The next_state may be + set to None in which case the current state will be unchanged. ''' + + if next_state is None: + next_state = state + for input_symbol in list_input_symbols: + self.add_transition (input_symbol, state, action, next_state) + + def add_transition_any (self, state, action=None, next_state=None): + + '''This adds a transition that associates: + + (current_state) --> (action, next_state) + + That is, any input symbol will match the current state. + The process() method checks the "any" state associations after it first + checks for an exact match of (input_symbol, current_state). + + The action may be set to None in which case the process() method will + ignore the action and only set the next_state. The next_state may be + set to None in which case the current state will be unchanged. ''' + + if next_state is None: + next_state = state + self.state_transitions_any [state] = (action, next_state) + + def set_default_transition (self, action, next_state): + + '''This sets the default transition. This defines an action and + next_state if the FSM cannot find the input symbol and the current + state in the transition list and if the FSM cannot find the + current_state in the transition_any list. This is useful as a final + fall-through state for catching errors and undefined states. + + The default transition can be removed by setting the attribute + default_transition to None. ''' + + self.default_transition = (action, next_state) + + def get_transition (self, input_symbol, state): + + '''This returns (action, next state) given an input_symbol and state. + This does not modify the FSM state, so calling this method has no side + effects. Normally you do not call this method directly. It is called by + process(). + + The sequence of steps to check for a defined transition goes from the + most specific to the least specific. + + 1. Check state_transitions[] that match exactly the tuple, + (input_symbol, state) + + 2. Check state_transitions_any[] that match (state) + In other words, match a specific state and ANY input_symbol. + + 3. Check if the default_transition is defined. + This catches any input_symbol and any state. + This is a handler for errors, undefined states, or defaults. + + 4. No transition was defined. If we get here then raise an exception. + ''' + + if (input_symbol, state) in self.state_transitions: + return self.state_transitions[(input_symbol, state)] + elif state in self.state_transitions_any: + return self.state_transitions_any[state] + elif self.default_transition is not None: + return self.default_transition + else: + raise ExceptionFSM ('Transition is undefined: (%s, %s).' % + (str(input_symbol), str(state)) ) + + def process (self, input_symbol): + + '''This is the main method that you call to process input. This may + cause the FSM to change state and call an action. This method calls + get_transition() to find the action and next_state associated with the + input_symbol and current_state. If the action is None then the action + is not called and only the current state is changed. This method + processes one complete input symbol. You can process a list of symbols + (or a string) by calling process_list(). ''' + + self.input_symbol = input_symbol + (self.action, self.next_state) = self.get_transition (self.input_symbol, self.current_state) + if self.action is not None: + self.action (self) + self.current_state = self.next_state + self.next_state = None + + def process_list (self, input_symbols): + + '''This takes a list and sends each element to process(). The list may + be a string or any iterable object. ''' + + for s in input_symbols: + self.process (s) + +############################################################################## +# The following is an example that demonstrates the use of the FSM class to +# process an RPN expression. Run this module from the command line. You will +# get a prompt > for input. Enter an RPN Expression. Numbers may be integers. +# Operators are * / + - Use the = sign to evaluate and print the expression. +# For example: +# +# 167 3 2 2 * * * 1 - = +# +# will print: +# +# 2003 +############################################################################## + +import sys +import string + +PY3 = (sys.version_info[0] >= 3) + +# +# These define the actions. +# Note that "memory" is a list being used as a stack. +# + +def BeginBuildNumber (fsm): + fsm.memory.append (fsm.input_symbol) + +def BuildNumber (fsm): + s = fsm.memory.pop () + s = s + fsm.input_symbol + fsm.memory.append (s) + +def EndBuildNumber (fsm): + s = fsm.memory.pop () + fsm.memory.append (int(s)) + +def DoOperator (fsm): + ar = fsm.memory.pop() + al = fsm.memory.pop() + if fsm.input_symbol == '+': + fsm.memory.append (al + ar) + elif fsm.input_symbol == '-': + fsm.memory.append (al - ar) + elif fsm.input_symbol == '*': + fsm.memory.append (al * ar) + elif fsm.input_symbol == '/': + fsm.memory.append (al / ar) + +def DoEqual (fsm): + print(str(fsm.memory.pop())) + +def Error (fsm): + print('That does not compute.') + print(str(fsm.input_symbol)) + +def main(): + + '''This is where the example starts and the FSM state transitions are + defined. Note that states are strings (such as 'INIT'). This is not + necessary, but it makes the example easier to read. ''' + + f = FSM ('INIT', []) + f.set_default_transition (Error, 'INIT') + f.add_transition_any ('INIT', None, 'INIT') + f.add_transition ('=', 'INIT', DoEqual, 'INIT') + f.add_transition_list (string.digits, 'INIT', BeginBuildNumber, 'BUILDING_NUMBER') + f.add_transition_list (string.digits, 'BUILDING_NUMBER', BuildNumber, 'BUILDING_NUMBER') + f.add_transition_list (string.whitespace, 'BUILDING_NUMBER', EndBuildNumber, 'INIT') + f.add_transition_list ('+-*/', 'INIT', DoOperator, 'INIT') + + print() + print('Enter an RPN Expression.') + print('Numbers may be integers. Operators are * / + -') + print('Use the = sign to evaluate and print the expression.') + print('For example: ') + print(' 167 3 2 2 * * * 1 - =') + inputstr = (input if PY3 else raw_input)('> ') # analysis:ignore + f.process_list(inputstr) + + +if __name__ == '__main__': + main() diff --git a/class/pexpect/__init__.py b/class/pexpect/__init__.py new file mode 100644 index 00000000..b3a64b68 --- /dev/null +++ b/class/pexpect/__init__.py @@ -0,0 +1,85 @@ +'''Pexpect is a Python module for spawning child applications and controlling +them automatically. Pexpect can be used for automating interactive applications +such as ssh, ftp, passwd, telnet, etc. It can be used to automate setup +scripts for duplicating software package installations on different servers. It +can be used for automated software testing. Pexpect is in the spirit of Don +Libes' Expect, but Pexpect is pure Python. Other Expect-like modules for Python +require TCL and Expect or require C extensions to be compiled. Pexpect does not +use C, Expect, or TCL extensions. It should work on any platform that supports +the standard Python pty module. The Pexpect interface focuses on ease of use so +that simple tasks are easy. + +There are two main interfaces to the Pexpect system; these are the function, +run() and the class, spawn. The spawn class is more powerful. The run() +function is simpler than spawn, and is good for quickly calling program. When +you call the run() function it executes a given program and then returns the +output. This is a handy replacement for os.system(). + +For example:: + + pexpect.run('ls -la') + +The spawn class is the more powerful interface to the Pexpect system. You can +use this to spawn a child program then interact with it by sending input and +expecting responses (waiting for patterns in the child's output). + +For example:: + + child = pexpect.spawn('scp foo user@example.com:.') + child.expect('Password:') + child.sendline(mypassword) + +This works even for commands that ask for passwords or other input outside of +the normal stdio streams. For example, ssh reads input directly from the TTY +device which bypasses stdin. + +Credits: Noah Spurrier, Richard Holden, Marco Molteni, Kimberley Burchett, +Robert Stone, Hartmut Goebel, Chad Schroeder, Erick Tryzelaar, Dave Kirby, Ids +vander Molen, George Todd, Noel Taylor, Nicolas D. Cesar, Alexander Gattin, +Jacques-Etienne Baudoux, Geoffrey Marshall, Francisco Lourenco, Glen Mabey, +Karthik Gurusamy, Fernando Perez, Corey Minyard, Jon Cohen, Guillaume +Chazarain, Andrew Ryan, Nick Craig-Wood, Andrew Stone, Jorgen Grahn, John +Spiegel, Jan Grant, and Shane Kerr. Let me know if I forgot anyone. + +Pexpect is free, open source, and all that good stuff. +http://pexpect.sourceforge.net/ + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +import sys +PY3 = (sys.version_info[0] >= 3) + +from .exceptions import ExceptionPexpect, EOF, TIMEOUT +from .utils import split_command_line, which, is_executable_file +from .expect import Expecter, searcher_re, searcher_string + +if sys.platform != 'win32': + # On Unix, these are available at the top level for backwards compatibility + from .pty_spawn import spawn, spawnu + from .run import run, runu + +__version__ = '4.8.0' +__revision__ = '' +__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'spawnu', 'run', 'runu', + 'which', 'split_command_line', '__version__', '__revision__'] + + + +# vim: set shiftround expandtab tabstop=4 shiftwidth=4 ft=python autoindent : diff --git a/class/pexpect/_async.py b/class/pexpect/_async.py new file mode 100644 index 00000000..dfbfeef5 --- /dev/null +++ b/class/pexpect/_async.py @@ -0,0 +1,103 @@ +import asyncio +import errno +import signal + +from pexpect import EOF + +@asyncio.coroutine +def expect_async(expecter, timeout=None): + # First process data that was previously read - if it maches, we don't need + # async stuff. + idx = expecter.existing_data() + if idx is not None: + return idx + if not expecter.spawn.async_pw_transport: + pw = PatternWaiter() + pw.set_expecter(expecter) + transport, pw = yield from asyncio.get_event_loop()\ + .connect_read_pipe(lambda: pw, expecter.spawn) + expecter.spawn.async_pw_transport = pw, transport + else: + pw, transport = expecter.spawn.async_pw_transport + pw.set_expecter(expecter) + transport.resume_reading() + try: + return (yield from asyncio.wait_for(pw.fut, timeout)) + except asyncio.TimeoutError as e: + transport.pause_reading() + return expecter.timeout(e) + +@asyncio.coroutine +def repl_run_command_async(repl, cmdlines, timeout=-1): + res = [] + repl.child.sendline(cmdlines[0]) + for line in cmdlines[1:]: + yield from repl._expect_prompt(timeout=timeout, async_=True) + res.append(repl.child.before) + repl.child.sendline(line) + + # Command was fully submitted, now wait for the next prompt + prompt_idx = yield from repl._expect_prompt(timeout=timeout, async_=True) + if prompt_idx == 1: + # We got the continuation prompt - command was incomplete + repl.child.kill(signal.SIGINT) + yield from repl._expect_prompt(timeout=1, async_=True) + raise ValueError("Continuation prompt found - input was incomplete:") + return u''.join(res + [repl.child.before]) + +class PatternWaiter(asyncio.Protocol): + transport = None + + def set_expecter(self, expecter): + self.expecter = expecter + self.fut = asyncio.Future() + + def found(self, result): + if not self.fut.done(): + self.fut.set_result(result) + self.transport.pause_reading() + + def error(self, exc): + if not self.fut.done(): + self.fut.set_exception(exc) + self.transport.pause_reading() + + def connection_made(self, transport): + self.transport = transport + + def data_received(self, data): + spawn = self.expecter.spawn + s = spawn._decoder.decode(data) + spawn._log(s, 'read') + + if self.fut.done(): + spawn._before.write(s) + spawn._buffer.write(s) + return + + try: + index = self.expecter.new_data(s) + if index is not None: + # Found a match + self.found(index) + except Exception as e: + self.expecter.errored() + self.error(e) + + def eof_received(self): + # N.B. If this gets called, async will close the pipe (the spawn object) + # for us + try: + self.expecter.spawn.flag_eof = True + index = self.expecter.eof() + except EOF as e: + self.error(e) + else: + self.found(index) + + def connection_lost(self, exc): + if isinstance(exc, OSError) and exc.errno == errno.EIO: + # We may get here without eof_received being called, e.g on Linux + self.eof_received() + elif exc is not None: + self.error(exc) diff --git a/class/pexpect/bashrc.sh b/class/pexpect/bashrc.sh new file mode 100644 index 00000000..c734ac90 --- /dev/null +++ b/class/pexpect/bashrc.sh @@ -0,0 +1,16 @@ +# Different platforms have different names for the systemwide bashrc +if [[ -f /etc/bashrc ]]; then + source /etc/bashrc +fi +if [[ -f /etc/bash.bashrc ]]; then + source /etc/bash.bashrc +fi +if [[ -f ~/.bashrc ]]; then + source ~/.bashrc +fi + +# Reset PS1 so pexpect can find it +PS1="$" + +# Unset PROMPT_COMMAND, so that it can't change PS1 to something unexpected. +unset PROMPT_COMMAND diff --git a/class/pexpect/exceptions.py b/class/pexpect/exceptions.py new file mode 100644 index 00000000..cb360f02 --- /dev/null +++ b/class/pexpect/exceptions.py @@ -0,0 +1,35 @@ +"""Exception classes used by Pexpect""" + +import traceback +import sys + +class ExceptionPexpect(Exception): + '''Base class for all exceptions raised by this module. + ''' + + def __init__(self, value): + super(ExceptionPexpect, self).__init__(value) + self.value = value + + def __str__(self): + return str(self.value) + + def get_trace(self): + '''This returns an abbreviated stack trace with lines that only concern + the caller. In other words, the stack trace inside the Pexpect module + is not included. ''' + + tblist = traceback.extract_tb(sys.exc_info()[2]) + tblist = [item for item in tblist if ('pexpect/__init__' not in item[0]) + and ('pexpect/expect' not in item[0])] + tblist = traceback.format_list(tblist) + return ''.join(tblist) + + +class EOF(ExceptionPexpect): + '''Raised when EOF is read from a child. + This usually means the child has exited.''' + + +class TIMEOUT(ExceptionPexpect): + '''Raised when a read time exceeds the timeout. ''' diff --git a/class/pexpect/expect.py b/class/pexpect/expect.py new file mode 100644 index 00000000..d3409db9 --- /dev/null +++ b/class/pexpect/expect.py @@ -0,0 +1,371 @@ +import time + +from .exceptions import EOF, TIMEOUT + +class Expecter(object): + def __init__(self, spawn, searcher, searchwindowsize=-1): + self.spawn = spawn + self.searcher = searcher + # A value of -1 means to use the figure from spawn, which should + # be None or a positive number. + if searchwindowsize == -1: + searchwindowsize = spawn.searchwindowsize + self.searchwindowsize = searchwindowsize + self.lookback = None + if hasattr(searcher, 'longest_string'): + self.lookback = searcher.longest_string + + def do_search(self, window, freshlen): + spawn = self.spawn + searcher = self.searcher + if freshlen > len(window): + freshlen = len(window) + index = searcher.search(window, freshlen, self.searchwindowsize) + if index >= 0: + spawn._buffer = spawn.buffer_type() + spawn._buffer.write(window[searcher.end:]) + spawn.before = spawn._before.getvalue()[ + 0:-(len(window) - searcher.start)] + spawn._before = spawn.buffer_type() + spawn._before.write(window[searcher.end:]) + spawn.after = window[searcher.start:searcher.end] + spawn.match = searcher.match + spawn.match_index = index + # Found a match + return index + elif self.searchwindowsize or self.lookback: + maintain = self.searchwindowsize or self.lookback + if spawn._buffer.tell() > maintain: + spawn._buffer = spawn.buffer_type() + spawn._buffer.write(window[-maintain:]) + + def existing_data(self): + # First call from a new call to expect_loop or expect_async. + # self.searchwindowsize may have changed. + # Treat all data as fresh. + spawn = self.spawn + before_len = spawn._before.tell() + buf_len = spawn._buffer.tell() + freshlen = before_len + if before_len > buf_len: + if not self.searchwindowsize: + spawn._buffer = spawn.buffer_type() + window = spawn._before.getvalue() + spawn._buffer.write(window) + elif buf_len < self.searchwindowsize: + spawn._buffer = spawn.buffer_type() + spawn._before.seek( + max(0, before_len - self.searchwindowsize)) + window = spawn._before.read() + spawn._buffer.write(window) + else: + spawn._buffer.seek(max(0, buf_len - self.searchwindowsize)) + window = spawn._buffer.read() + else: + if self.searchwindowsize: + spawn._buffer.seek(max(0, buf_len - self.searchwindowsize)) + window = spawn._buffer.read() + else: + window = spawn._buffer.getvalue() + return self.do_search(window, freshlen) + + def new_data(self, data): + # A subsequent call, after a call to existing_data. + spawn = self.spawn + freshlen = len(data) + spawn._before.write(data) + if not self.searchwindowsize: + if self.lookback: + # search lookback + new data. + old_len = spawn._buffer.tell() + spawn._buffer.write(data) + spawn._buffer.seek(max(0, old_len - self.lookback)) + window = spawn._buffer.read() + else: + # copy the whole buffer (really slow for large datasets). + spawn._buffer.write(data) + window = spawn.buffer + else: + if len(data) >= self.searchwindowsize or not spawn._buffer.tell(): + window = data[-self.searchwindowsize:] + spawn._buffer = spawn.buffer_type() + spawn._buffer.write(window[-self.searchwindowsize:]) + else: + spawn._buffer.write(data) + new_len = spawn._buffer.tell() + spawn._buffer.seek(max(0, new_len - self.searchwindowsize)) + window = spawn._buffer.read() + return self.do_search(window, freshlen) + + def eof(self, err=None): + spawn = self.spawn + + spawn.before = spawn._before.getvalue() + spawn._buffer = spawn.buffer_type() + spawn._before = spawn.buffer_type() + spawn.after = EOF + index = self.searcher.eof_index + if index >= 0: + spawn.match = EOF + spawn.match_index = index + return index + else: + spawn.match = None + spawn.match_index = None + msg = str(spawn) + msg += '\nsearcher: %s' % self.searcher + if err is not None: + msg = str(err) + '\n' + msg + + exc = EOF(msg) + exc.__cause__ = None # in Python 3.x we can use "raise exc from None" + raise exc + + def timeout(self, err=None): + spawn = self.spawn + + spawn.before = spawn._before.getvalue() + spawn.after = TIMEOUT + index = self.searcher.timeout_index + if index >= 0: + spawn.match = TIMEOUT + spawn.match_index = index + return index + else: + spawn.match = None + spawn.match_index = None + msg = str(spawn) + msg += '\nsearcher: %s' % self.searcher + if err is not None: + msg = str(err) + '\n' + msg + + exc = TIMEOUT(msg) + exc.__cause__ = None # in Python 3.x we can use "raise exc from None" + raise exc + + def errored(self): + spawn = self.spawn + spawn.before = spawn._before.getvalue() + spawn.after = None + spawn.match = None + spawn.match_index = None + + def expect_loop(self, timeout=-1): + """Blocking expect""" + spawn = self.spawn + + if timeout is not None: + end_time = time.time() + timeout + + try: + idx = self.existing_data() + if idx is not None: + return idx + while True: + # No match at this point + if (timeout is not None) and (timeout < 0): + return self.timeout() + # Still have time left, so read more data + incoming = spawn.read_nonblocking(spawn.maxread, timeout) + if self.spawn.delayafterread is not None: + time.sleep(self.spawn.delayafterread) + idx = self.new_data(incoming) + # Keep reading until exception or return. + if idx is not None: + return idx + if timeout is not None: + timeout = end_time - time.time() + except EOF as e: + return self.eof(e) + except TIMEOUT as e: + return self.timeout(e) + except: + self.errored() + raise + + +class searcher_string(object): + '''This is a plain string search helper for the spawn.expect_any() method. + This helper class is for speed. For more powerful regex patterns + see the helper class, searcher_re. + + Attributes: + + eof_index - index of EOF, or -1 + timeout_index - index of TIMEOUT, or -1 + + After a successful match by the search() method the following attributes + are available: + + start - index into the buffer, first byte of match + end - index into the buffer, first byte after match + match - the matching string itself + + ''' + + def __init__(self, strings): + '''This creates an instance of searcher_string. This argument 'strings' + may be a list; a sequence of strings; or the EOF or TIMEOUT types. ''' + + self.eof_index = -1 + self.timeout_index = -1 + self._strings = [] + self.longest_string = 0 + for n, s in enumerate(strings): + if s is EOF: + self.eof_index = n + continue + if s is TIMEOUT: + self.timeout_index = n + continue + self._strings.append((n, s)) + if len(s) > self.longest_string: + self.longest_string = len(s) + + def __str__(self): + '''This returns a human-readable string that represents the state of + the object.''' + + ss = [(ns[0], ' %d: %r' % ns) for ns in self._strings] + ss.append((-1, 'searcher_string:')) + if self.eof_index >= 0: + ss.append((self.eof_index, ' %d: EOF' % self.eof_index)) + if self.timeout_index >= 0: + ss.append((self.timeout_index, + ' %d: TIMEOUT' % self.timeout_index)) + ss.sort() + ss = list(zip(*ss))[1] + return '\n'.join(ss) + + def search(self, buffer, freshlen, searchwindowsize=None): + '''This searches 'buffer' for the first occurrence of one of the search + strings. 'freshlen' must indicate the number of bytes at the end of + 'buffer' which have not been searched before. It helps to avoid + searching the same, possibly big, buffer over and over again. + + See class spawn for the 'searchwindowsize' argument. + + If there is a match this returns the index of that string, and sets + 'start', 'end' and 'match'. Otherwise, this returns -1. ''' + + first_match = None + + # 'freshlen' helps a lot here. Further optimizations could + # possibly include: + # + # using something like the Boyer-Moore Fast String Searching + # Algorithm; pre-compiling the search through a list of + # strings into something that can scan the input once to + # search for all N strings; realize that if we search for + # ['bar', 'baz'] and the input is '...foo' we need not bother + # rescanning until we've read three more bytes. + # + # Sadly, I don't know enough about this interesting topic. /grahn + + for index, s in self._strings: + if searchwindowsize is None: + # the match, if any, can only be in the fresh data, + # or at the very end of the old data + offset = -(freshlen + len(s)) + else: + # better obey searchwindowsize + offset = -searchwindowsize + n = buffer.find(s, offset) + if n >= 0 and (first_match is None or n < first_match): + first_match = n + best_index, best_match = index, s + if first_match is None: + return -1 + self.match = best_match + self.start = first_match + self.end = self.start + len(self.match) + return best_index + + +class searcher_re(object): + '''This is regular expression string search helper for the + spawn.expect_any() method. This helper class is for powerful + pattern matching. For speed, see the helper class, searcher_string. + + Attributes: + + eof_index - index of EOF, or -1 + timeout_index - index of TIMEOUT, or -1 + + After a successful match by the search() method the following attributes + are available: + + start - index into the buffer, first byte of match + end - index into the buffer, first byte after match + match - the re.match object returned by a successful re.search + + ''' + + def __init__(self, patterns): + '''This creates an instance that searches for 'patterns' Where + 'patterns' may be a list or other sequence of compiled regular + expressions, or the EOF or TIMEOUT types.''' + + self.eof_index = -1 + self.timeout_index = -1 + self._searches = [] + for n, s in enumerate(patterns): + if s is EOF: + self.eof_index = n + continue + if s is TIMEOUT: + self.timeout_index = n + continue + self._searches.append((n, s)) + + def __str__(self): + '''This returns a human-readable string that represents the state of + the object.''' + + #ss = [(n, ' %d: re.compile("%s")' % + # (n, repr(s.pattern))) for n, s in self._searches] + ss = list() + for n, s in self._searches: + ss.append((n, ' %d: re.compile(%r)' % (n, s.pattern))) + ss.append((-1, 'searcher_re:')) + if self.eof_index >= 0: + ss.append((self.eof_index, ' %d: EOF' % self.eof_index)) + if self.timeout_index >= 0: + ss.append((self.timeout_index, ' %d: TIMEOUT' % + self.timeout_index)) + ss.sort() + ss = list(zip(*ss))[1] + return '\n'.join(ss) + + def search(self, buffer, freshlen, searchwindowsize=None): + '''This searches 'buffer' for the first occurrence of one of the regular + expressions. 'freshlen' must indicate the number of bytes at the end of + 'buffer' which have not been searched before. + + See class spawn for the 'searchwindowsize' argument. + + If there is a match this returns the index of that string, and sets + 'start', 'end' and 'match'. Otherwise, returns -1.''' + + first_match = None + # 'freshlen' doesn't help here -- we cannot predict the + # length of a match, and the re module provides no help. + if searchwindowsize is None: + searchstart = 0 + else: + searchstart = max(0, len(buffer) - searchwindowsize) + for index, s in self._searches: + match = s.search(buffer, searchstart) + if match is None: + continue + n = match.start() + if first_match is None or n < first_match: + first_match = n + the_match = match + best_index = index + if first_match is None: + return -1 + self.start = first_match + self.match = the_match + self.end = self.match.end() + return best_index diff --git a/class/pexpect/fdpexpect.py b/class/pexpect/fdpexpect.py new file mode 100644 index 00000000..cddd50e1 --- /dev/null +++ b/class/pexpect/fdpexpect.py @@ -0,0 +1,148 @@ +'''This is like pexpect, but it will work with any file descriptor that you +pass it. You are responsible for opening and close the file descriptor. +This allows you to use Pexpect with sockets and named pipes (FIFOs). + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +from .spawnbase import SpawnBase +from .exceptions import ExceptionPexpect, TIMEOUT +from .utils import select_ignore_interrupts, poll_ignore_interrupts +import os + +__all__ = ['fdspawn'] + +class fdspawn(SpawnBase): + '''This is like pexpect.spawn but allows you to supply your own open file + descriptor. For example, you could use it to read through a file looking + for patterns, or to control a modem or serial device. ''' + + def __init__ (self, fd, args=None, timeout=30, maxread=2000, searchwindowsize=None, + logfile=None, encoding=None, codec_errors='strict', use_poll=False): + '''This takes a file descriptor (an int) or an object that support the + fileno() method (returning an int). All Python file-like objects + support fileno(). ''' + + if type(fd) != type(0) and hasattr(fd, 'fileno'): + fd = fd.fileno() + + if type(fd) != type(0): + raise ExceptionPexpect('The fd argument is not an int. If this is a command string then maybe you want to use pexpect.spawn.') + + try: # make sure fd is a valid file descriptor + os.fstat(fd) + except OSError: + raise ExceptionPexpect('The fd argument is not a valid file descriptor.') + + self.args = None + self.command = None + SpawnBase.__init__(self, timeout, maxread, searchwindowsize, logfile, + encoding=encoding, codec_errors=codec_errors) + self.child_fd = fd + self.own_fd = False + self.closed = False + self.name = '' % fd + self.use_poll = use_poll + + def close (self): + """Close the file descriptor. + + Calling this method a second time does nothing, but if the file + descriptor was closed elsewhere, :class:`OSError` will be raised. + """ + if self.child_fd == -1: + return + + self.flush() + os.close(self.child_fd) + self.child_fd = -1 + self.closed = True + + def isalive (self): + '''This checks if the file descriptor is still valid. If :func:`os.fstat` + does not raise an exception then we assume it is alive. ''' + + if self.child_fd == -1: + return False + try: + os.fstat(self.child_fd) + return True + except: + return False + + def terminate (self, force=False): # pragma: no cover + '''Deprecated and invalid. Just raises an exception.''' + raise ExceptionPexpect('This method is not valid for file descriptors.') + + # These four methods are left around for backwards compatibility, but not + # documented as part of fdpexpect. You're encouraged to use os.write + # directly. + def send(self, s): + "Write to fd, return number of bytes written" + s = self._coerce_send_string(s) + self._log(s, 'send') + + b = self._encoder.encode(s, final=False) + return os.write(self.child_fd, b) + + def sendline(self, s): + "Write to fd with trailing newline, return number of bytes written" + s = self._coerce_send_string(s) + return self.send(s + self.linesep) + + def write(self, s): + "Write to fd, return None" + self.send(s) + + def writelines(self, sequence): + "Call self.write() for each item in sequence" + for s in sequence: + self.write(s) + + def read_nonblocking(self, size=1, timeout=-1): + """ + Read from the file descriptor and return the result as a string. + + The read_nonblocking method of :class:`SpawnBase` assumes that a call + to os.read will not block (timeout parameter is ignored). This is not + the case for POSIX file-like objects such as sockets and serial ports. + + Use :func:`select.select`, timeout is implemented conditionally for + POSIX systems. + + :param int size: Read at most *size* bytes. + :param int timeout: Wait timeout seconds for file descriptor to be + ready to read. When -1 (default), use self.timeout. When 0, poll. + :return: String containing the bytes read + """ + if os.name == 'posix': + if timeout == -1: + timeout = self.timeout + rlist = [self.child_fd] + wlist = [] + xlist = [] + if self.use_poll: + rlist = poll_ignore_interrupts(rlist, timeout) + else: + rlist, wlist, xlist = select_ignore_interrupts( + rlist, wlist, xlist, timeout + ) + if self.child_fd not in rlist: + raise TIMEOUT('Timeout exceeded.') + return super(fdspawn, self).read_nonblocking(size) diff --git a/class/pexpect/popen_spawn.py b/class/pexpect/popen_spawn.py new file mode 100644 index 00000000..4bb58cfe --- /dev/null +++ b/class/pexpect/popen_spawn.py @@ -0,0 +1,188 @@ +"""Provides an interface like pexpect.spawn interface using subprocess.Popen +""" +import os +import threading +import subprocess +import sys +import time +import signal +import shlex + +try: + from queue import Queue, Empty # Python 3 +except ImportError: + from Queue import Queue, Empty # Python 2 + +from .spawnbase import SpawnBase, PY3 +from .exceptions import EOF +from .utils import string_types + +class PopenSpawn(SpawnBase): + def __init__(self, cmd, timeout=30, maxread=2000, searchwindowsize=None, + logfile=None, cwd=None, env=None, encoding=None, + codec_errors='strict', preexec_fn=None): + super(PopenSpawn, self).__init__(timeout=timeout, maxread=maxread, + searchwindowsize=searchwindowsize, logfile=logfile, + encoding=encoding, codec_errors=codec_errors) + + # Note that `SpawnBase` initializes `self.crlf` to `\r\n` + # because the default behaviour for a PTY is to convert + # incoming LF to `\r\n` (see the `onlcr` flag and + # https://stackoverflow.com/a/35887657/5397009). Here we set + # it to `os.linesep` because that is what the spawned + # application outputs by default and `popen` doesn't translate + # anything. + if encoding is None: + self.crlf = os.linesep.encode ("ascii") + else: + self.crlf = self.string_type (os.linesep) + + kwargs = dict(bufsize=0, stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, stdout=subprocess.PIPE, + cwd=cwd, preexec_fn=preexec_fn, env=env) + + if sys.platform == 'win32': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + kwargs['startupinfo'] = startupinfo + kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP + + if isinstance(cmd, string_types) and sys.platform != 'win32': + cmd = shlex.split(cmd, posix=os.name == 'posix') + + self.proc = subprocess.Popen(cmd, **kwargs) + self.pid = self.proc.pid + self.closed = False + self._buf = self.string_type() + + self._read_queue = Queue() + self._read_thread = threading.Thread(target=self._read_incoming) + self._read_thread.setDaemon(True) + self._read_thread.start() + + _read_reached_eof = False + + def read_nonblocking(self, size, timeout): + buf = self._buf + if self._read_reached_eof: + # We have already finished reading. Use up any buffered data, + # then raise EOF + if buf: + self._buf = buf[size:] + return buf[:size] + else: + self.flag_eof = True + raise EOF('End Of File (EOF).') + + if timeout == -1: + timeout = self.timeout + elif timeout is None: + timeout = 1e6 + + t0 = time.time() + while (time.time() - t0) < timeout and size and len(buf) < size: + try: + incoming = self._read_queue.get_nowait() + except Empty: + break + else: + if incoming is None: + self._read_reached_eof = True + break + + buf += self._decoder.decode(incoming, final=False) + + r, self._buf = buf[:size], buf[size:] + + self._log(r, 'read') + return r + + def _read_incoming(self): + """Run in a thread to move output from a pipe to a queue.""" + fileno = self.proc.stdout.fileno() + while 1: + buf = b'' + try: + buf = os.read(fileno, 1024) + except OSError as e: + self._log(e, 'read') + + if not buf: + # This indicates we have reached EOF + self._read_queue.put(None) + return + + self._read_queue.put(buf) + + def write(self, s): + '''This is similar to send() except that there is no return value. + ''' + self.send(s) + + def writelines(self, sequence): + '''This calls write() for each element in the sequence. + + The sequence can be any iterable object producing strings, typically a + list of strings. This does not add line separators. There is no return + value. + ''' + for s in sequence: + self.send(s) + + def send(self, s): + '''Send data to the subprocess' stdin. + + Returns the number of bytes written. + ''' + s = self._coerce_send_string(s) + self._log(s, 'send') + + b = self._encoder.encode(s, final=False) + if PY3: + return self.proc.stdin.write(b) + else: + # On Python 2, .write() returns None, so we return the length of + # bytes written ourselves. This assumes they all got written. + self.proc.stdin.write(b) + return len(b) + + def sendline(self, s=''): + '''Wraps send(), sending string ``s`` to child process, with os.linesep + automatically appended. Returns number of bytes written. ''' + + n = self.send(s) + return n + self.send(self.linesep) + + def wait(self): + '''Wait for the subprocess to finish. + + Returns the exit code. + ''' + status = self.proc.wait() + if status >= 0: + self.exitstatus = status + self.signalstatus = None + else: + self.exitstatus = None + self.signalstatus = -status + self.terminated = True + return status + + def kill(self, sig): + '''Sends a Unix signal to the subprocess. + + Use constants from the :mod:`signal` module to specify which signal. + ''' + if sys.platform == 'win32': + if sig in [signal.SIGINT, signal.CTRL_C_EVENT]: + sig = signal.CTRL_C_EVENT + elif sig in [signal.SIGBREAK, signal.CTRL_BREAK_EVENT]: + sig = signal.CTRL_BREAK_EVENT + else: + sig = signal.SIGTERM + + os.kill(self.proc.pid, sig) + + def sendeof(self): + '''Closes the stdin pipe from the writing end.''' + self.proc.stdin.close() diff --git a/class/pexpect/pty_spawn.py b/class/pexpect/pty_spawn.py new file mode 100644 index 00000000..8e28ca7c --- /dev/null +++ b/class/pexpect/pty_spawn.py @@ -0,0 +1,860 @@ +import os +import sys +import time +import pty +import tty +import errno +import signal +from contextlib import contextmanager + +import ptyprocess +from ptyprocess.ptyprocess import use_native_pty_fork + +from .exceptions import ExceptionPexpect, EOF, TIMEOUT +from .spawnbase import SpawnBase +from .utils import ( + which, split_command_line, select_ignore_interrupts, poll_ignore_interrupts +) + +@contextmanager +def _wrap_ptyprocess_err(): + """Turn ptyprocess errors into our own ExceptionPexpect errors""" + try: + yield + except ptyprocess.PtyProcessError as e: + raise ExceptionPexpect(*e.args) + +PY3 = (sys.version_info[0] >= 3) + +class spawn(SpawnBase): + '''This is the main class interface for Pexpect. Use this class to start + and control child applications. ''' + + # This is purely informational now - changing it has no effect + use_native_pty_fork = use_native_pty_fork + + def __init__(self, command, args=[], timeout=30, maxread=2000, + searchwindowsize=None, logfile=None, cwd=None, env=None, + ignore_sighup=False, echo=True, preexec_fn=None, + encoding=None, codec_errors='strict', dimensions=None, + use_poll=False): + '''This is the constructor. The command parameter may be a string that + includes a command and any arguments to the command. For example:: + + child = pexpect.spawn('/usr/bin/ftp') + child = pexpect.spawn('/usr/bin/ssh user@example.com') + child = pexpect.spawn('ls -latr /tmp') + + You may also construct it with a list of arguments like so:: + + child = pexpect.spawn('/usr/bin/ftp', []) + child = pexpect.spawn('/usr/bin/ssh', ['user@example.com']) + child = pexpect.spawn('ls', ['-latr', '/tmp']) + + After this the child application will be created and will be ready to + talk to. For normal use, see expect() and send() and sendline(). + + Remember that Pexpect does NOT interpret shell meta characters such as + redirect, pipe, or wild cards (``>``, ``|``, or ``*``). This is a + common mistake. If you want to run a command and pipe it through + another command then you must also start a shell. For example:: + + child = pexpect.spawn('/bin/bash -c "ls -l | grep LOG > logs.txt"') + child.expect(pexpect.EOF) + + The second form of spawn (where you pass a list of arguments) is useful + in situations where you wish to spawn a command and pass it its own + argument list. This can make syntax more clear. For example, the + following is equivalent to the previous example:: + + shell_cmd = 'ls -l | grep LOG > logs.txt' + child = pexpect.spawn('/bin/bash', ['-c', shell_cmd]) + child.expect(pexpect.EOF) + + The maxread attribute sets the read buffer size. This is maximum number + of bytes that Pexpect will try to read from a TTY at one time. Setting + the maxread size to 1 will turn off buffering. Setting the maxread + value higher may help performance in cases where large amounts of + output are read back from the child. This feature is useful in + conjunction with searchwindowsize. + + When the keyword argument *searchwindowsize* is None (default), the + full buffer is searched at each iteration of receiving incoming data. + The default number of bytes scanned at each iteration is very large + and may be reduced to collaterally reduce search cost. After + :meth:`~.expect` returns, the full buffer attribute remains up to + size *maxread* irrespective of *searchwindowsize* value. + + When the keyword argument ``timeout`` is specified as a number, + (default: *30*), then :class:`TIMEOUT` will be raised after the value + specified has elapsed, in seconds, for any of the :meth:`~.expect` + family of method calls. When None, TIMEOUT will not be raised, and + :meth:`~.expect` may block indefinitely until match. + + + The logfile member turns on or off logging. All input and output will + be copied to the given file object. Set logfile to None to stop + logging. This is the default. Set logfile to sys.stdout to echo + everything to standard output. The logfile is flushed after each write. + + Example log input and output to a file:: + + child = pexpect.spawn('some_command') + fout = open('mylog.txt','wb') + child.logfile = fout + + Example log to stdout:: + + # In Python 2: + child = pexpect.spawn('some_command') + child.logfile = sys.stdout + + # In Python 3, we'll use the ``encoding`` argument to decode data + # from the subprocess and handle it as unicode: + child = pexpect.spawn('some_command', encoding='utf-8') + child.logfile = sys.stdout + + The logfile_read and logfile_send members can be used to separately log + the input from the child and output sent to the child. Sometimes you + don't want to see everything you write to the child. You only want to + log what the child sends back. For example:: + + child = pexpect.spawn('some_command') + child.logfile_read = sys.stdout + + You will need to pass an encoding to spawn in the above code if you are + using Python 3. + + To separately log output sent to the child use logfile_send:: + + child.logfile_send = fout + + If ``ignore_sighup`` is True, the child process will ignore SIGHUP + signals. The default is False from Pexpect 4.0, meaning that SIGHUP + will be handled normally by the child. + + The delaybeforesend helps overcome a weird behavior that many users + were experiencing. The typical problem was that a user would expect() a + "Password:" prompt and then immediately call sendline() to send the + password. The user would then see that their password was echoed back + to them. Passwords don't normally echo. The problem is caused by the + fact that most applications print out the "Password" prompt and then + turn off stdin echo, but if you send your password before the + application turned off echo, then you get your password echoed. + Normally this wouldn't be a problem when interacting with a human at a + real keyboard. If you introduce a slight delay just before writing then + this seems to clear up the problem. This was such a common problem for + many users that I decided that the default pexpect behavior should be + to sleep just before writing to the child application. 1/20th of a + second (50 ms) seems to be enough to clear up the problem. You can set + delaybeforesend to None to return to the old behavior. + + Note that spawn is clever about finding commands on your path. + It uses the same logic that "which" uses to find executables. + + If you wish to get the exit status of the child you must call the + close() method. The exit or signal status of the child will be stored + in self.exitstatus or self.signalstatus. If the child exited normally + then exitstatus will store the exit return code and signalstatus will + be None. If the child was terminated abnormally with a signal then + signalstatus will store the signal value and exitstatus will be None:: + + child = pexpect.spawn('some_command') + child.close() + print(child.exitstatus, child.signalstatus) + + If you need more detail you can also read the self.status member which + stores the status returned by os.waitpid. You can interpret this using + os.WIFEXITED/os.WEXITSTATUS or os.WIFSIGNALED/os.TERMSIG. + + The echo attribute may be set to False to disable echoing of input. + As a pseudo-terminal, all input echoed by the "keyboard" (send() + or sendline()) will be repeated to output. For many cases, it is + not desirable to have echo enabled, and it may be later disabled + using setecho(False) followed by waitnoecho(). However, for some + platforms such as Solaris, this is not possible, and should be + disabled immediately on spawn. + + If preexec_fn is given, it will be called in the child process before + launching the given command. This is useful to e.g. reset inherited + signal handlers. + + The dimensions attribute specifies the size of the pseudo-terminal as + seen by the subprocess, and is specified as a two-entry tuple (rows, + columns). If this is unspecified, the defaults in ptyprocess will apply. + + The use_poll attribute enables using select.poll() over select.select() + for socket handling. This is handy if your system could have > 1024 fds + ''' + super(spawn, self).__init__(timeout=timeout, maxread=maxread, searchwindowsize=searchwindowsize, + logfile=logfile, encoding=encoding, codec_errors=codec_errors) + self.STDIN_FILENO = pty.STDIN_FILENO + self.STDOUT_FILENO = pty.STDOUT_FILENO + self.STDERR_FILENO = pty.STDERR_FILENO + self.str_last_chars = 100 + self.cwd = cwd + self.env = env + self.echo = echo + self.ignore_sighup = ignore_sighup + self.__irix_hack = sys.platform.lower().startswith('irix') + if command is None: + self.command = None + self.args = None + self.name = '' + else: + self._spawn(command, args, preexec_fn, dimensions) + self.use_poll = use_poll + + def __str__(self): + '''This returns a human-readable string that represents the state of + the object. ''' + + s = [] + s.append(repr(self)) + s.append('command: ' + str(self.command)) + s.append('args: %r' % (self.args,)) + s.append('buffer (last %s chars): %r' % (self.str_last_chars,self.buffer[-self.str_last_chars:])) + s.append('before (last %s chars): %r' % (self.str_last_chars,self.before[-self.str_last_chars:] if self.before else '')) + s.append('after: %r' % (self.after,)) + s.append('match: %r' % (self.match,)) + s.append('match_index: ' + str(self.match_index)) + s.append('exitstatus: ' + str(self.exitstatus)) + if hasattr(self, 'ptyproc'): + s.append('flag_eof: ' + str(self.flag_eof)) + s.append('pid: ' + str(self.pid)) + s.append('child_fd: ' + str(self.child_fd)) + s.append('closed: ' + str(self.closed)) + s.append('timeout: ' + str(self.timeout)) + s.append('delimiter: ' + str(self.delimiter)) + s.append('logfile: ' + str(self.logfile)) + s.append('logfile_read: ' + str(self.logfile_read)) + s.append('logfile_send: ' + str(self.logfile_send)) + s.append('maxread: ' + str(self.maxread)) + s.append('ignorecase: ' + str(self.ignorecase)) + s.append('searchwindowsize: ' + str(self.searchwindowsize)) + s.append('delaybeforesend: ' + str(self.delaybeforesend)) + s.append('delayafterclose: ' + str(self.delayafterclose)) + s.append('delayafterterminate: ' + str(self.delayafterterminate)) + return '\n'.join(s) + + def _spawn(self, command, args=[], preexec_fn=None, dimensions=None): + '''This starts the given command in a child process. This does all the + fork/exec type of stuff for a pty. This is called by __init__. If args + is empty then command will be parsed (split on spaces) and args will be + set to parsed arguments. ''' + + # The pid and child_fd of this object get set by this method. + # Note that it is difficult for this method to fail. + # You cannot detect if the child process cannot start. + # So the only way you can tell if the child process started + # or not is to try to read from the file descriptor. If you get + # EOF immediately then it means that the child is already dead. + # That may not necessarily be bad because you may have spawned a child + # that performs some task; creates no stdout output; and then dies. + + # If command is an int type then it may represent a file descriptor. + if isinstance(command, type(0)): + raise ExceptionPexpect('Command is an int type. ' + + 'If this is a file descriptor then maybe you want to ' + + 'use fdpexpect.fdspawn which takes an existing ' + + 'file descriptor instead of a command string.') + + if not isinstance(args, type([])): + raise TypeError('The argument, args, must be a list.') + + if args == []: + self.args = split_command_line(command) + self.command = self.args[0] + else: + # Make a shallow copy of the args list. + self.args = args[:] + self.args.insert(0, command) + self.command = command + + command_with_path = which(self.command, env=self.env) + if command_with_path is None: + raise ExceptionPexpect('The command was not found or was not ' + + 'executable: %s.' % self.command) + self.command = command_with_path + self.args[0] = self.command + + self.name = '<' + ' '.join(self.args) + '>' + + assert self.pid is None, 'The pid member must be None.' + assert self.command is not None, 'The command member must not be None.' + + kwargs = {'echo': self.echo, 'preexec_fn': preexec_fn} + if self.ignore_sighup: + def preexec_wrapper(): + "Set SIGHUP to be ignored, then call the real preexec_fn" + signal.signal(signal.SIGHUP, signal.SIG_IGN) + if preexec_fn is not None: + preexec_fn() + kwargs['preexec_fn'] = preexec_wrapper + + if dimensions is not None: + kwargs['dimensions'] = dimensions + + if self.encoding is not None: + # Encode command line using the specified encoding + self.args = [a if isinstance(a, bytes) else a.encode(self.encoding) + for a in self.args] + + self.ptyproc = self._spawnpty(self.args, env=self.env, + cwd=self.cwd, **kwargs) + + self.pid = self.ptyproc.pid + self.child_fd = self.ptyproc.fd + + + self.terminated = False + self.closed = False + + def _spawnpty(self, args, **kwargs): + '''Spawn a pty and return an instance of PtyProcess.''' + return ptyprocess.PtyProcess.spawn(args, **kwargs) + + def close(self, force=True): + '''This closes the connection with the child application. Note that + calling close() more than once is valid. This emulates standard Python + behavior with files. Set force to True if you want to make sure that + the child is terminated (SIGKILL is sent if the child ignores SIGHUP + and SIGINT). ''' + + self.flush() + with _wrap_ptyprocess_err(): + # PtyProcessError may be raised if it is not possible to terminate + # the child. + self.ptyproc.close(force=force) + self.isalive() # Update exit status from ptyproc + self.child_fd = -1 + self.closed = True + + def isatty(self): + '''This returns True if the file descriptor is open and connected to a + tty(-like) device, else False. + + On SVR4-style platforms implementing streams, such as SunOS and HP-UX, + the child pty may not appear as a terminal device. This means + methods such as setecho(), setwinsize(), getwinsize() may raise an + IOError. ''' + + return os.isatty(self.child_fd) + + def waitnoecho(self, timeout=-1): + '''This waits until the terminal ECHO flag is set False. This returns + True if the echo mode is off. This returns False if the ECHO flag was + not set False before the timeout. This can be used to detect when the + child is waiting for a password. Usually a child application will turn + off echo mode when it is waiting for the user to enter a password. For + example, instead of expecting the "password:" prompt you can wait for + the child to set ECHO off:: + + p = pexpect.spawn('ssh user@example.com') + p.waitnoecho() + p.sendline(mypassword) + + If timeout==-1 then this method will use the value in self.timeout. + If timeout==None then this method to block until ECHO flag is False. + ''' + + if timeout == -1: + timeout = self.timeout + if timeout is not None: + end_time = time.time() + timeout + while True: + if not self.getecho(): + return True + if timeout < 0 and timeout is not None: + return False + if timeout is not None: + timeout = end_time - time.time() + time.sleep(0.1) + + def getecho(self): + '''This returns the terminal echo mode. This returns True if echo is + on or False if echo is off. Child applications that are expecting you + to enter a password often set ECHO False. See waitnoecho(). + + Not supported on platforms where ``isatty()`` returns False. ''' + return self.ptyproc.getecho() + + def setecho(self, state): + '''This sets the terminal echo mode on or off. Note that anything the + child sent before the echo will be lost, so you should be sure that + your input buffer is empty before you call setecho(). For example, the + following will work as expected:: + + p = pexpect.spawn('cat') # Echo is on by default. + p.sendline('1234') # We expect see this twice from the child... + p.expect(['1234']) # ... once from the tty echo... + p.expect(['1234']) # ... and again from cat itself. + p.setecho(False) # Turn off tty echo + p.sendline('abcd') # We will set this only once (echoed by cat). + p.sendline('wxyz') # We will set this only once (echoed by cat) + p.expect(['abcd']) + p.expect(['wxyz']) + + The following WILL NOT WORK because the lines sent before the setecho + will be lost:: + + p = pexpect.spawn('cat') + p.sendline('1234') + p.setecho(False) # Turn off tty echo + p.sendline('abcd') # We will set this only once (echoed by cat). + p.sendline('wxyz') # We will set this only once (echoed by cat) + p.expect(['1234']) + p.expect(['1234']) + p.expect(['abcd']) + p.expect(['wxyz']) + + + Not supported on platforms where ``isatty()`` returns False. + ''' + return self.ptyproc.setecho(state) + + def read_nonblocking(self, size=1, timeout=-1): + '''This reads at most size characters from the child application. It + includes a timeout. If the read does not complete within the timeout + period then a TIMEOUT exception is raised. If the end of file is read + then an EOF exception will be raised. If a logfile is specified, a + copy is written to that log. + + If timeout is None then the read may block indefinitely. + If timeout is -1 then the self.timeout value is used. If timeout is 0 + then the child is polled and if there is no data immediately ready + then this will raise a TIMEOUT exception. + + The timeout refers only to the amount of time to read at least one + character. This is not affected by the 'size' parameter, so if you call + read_nonblocking(size=100, timeout=30) and only one character is + available right away then one character will be returned immediately. + It will not wait for 30 seconds for another 99 characters to come in. + + On the other hand, if there are bytes available to read immediately, + all those bytes will be read (up to the buffer size). So, if the + buffer size is 1 megabyte and there is 1 megabyte of data available + to read, the buffer will be filled, regardless of timeout. + + This is a wrapper around os.read(). It uses select.select() or + select.poll() to implement the timeout. ''' + + if self.closed: + raise ValueError('I/O operation on closed file.') + + if self.use_poll: + def select(timeout): + return poll_ignore_interrupts([self.child_fd], timeout) + else: + def select(timeout): + return select_ignore_interrupts([self.child_fd], [], [], timeout)[0] + + # If there is data available to read right now, read as much as + # we can. We do this to increase performance if there are a lot + # of bytes to be read. This also avoids calling isalive() too + # often. See also: + # * https://github.com/pexpect/pexpect/pull/304 + # * http://trac.sagemath.org/ticket/10295 + if select(0): + try: + incoming = super(spawn, self).read_nonblocking(size) + except EOF: + # Maybe the child is dead: update some attributes in that case + self.isalive() + raise + while len(incoming) < size and select(0): + try: + incoming += super(spawn, self).read_nonblocking(size - len(incoming)) + except EOF: + # Maybe the child is dead: update some attributes in that case + self.isalive() + # Don't raise EOF, just return what we read so far. + return incoming + return incoming + + if timeout == -1: + timeout = self.timeout + + if not self.isalive(): + # The process is dead, but there may or may not be data + # available to read. Note that some systems such as Solaris + # do not give an EOF when the child dies. In fact, you can + # still try to read from the child_fd -- it will block + # forever or until TIMEOUT. For that reason, it's important + # to do this check before calling select() with timeout. + if select(0): + return super(spawn, self).read_nonblocking(size) + self.flag_eof = True + raise EOF('End Of File (EOF). Braindead platform.') + elif self.__irix_hack: + # Irix takes a long time before it realizes a child was terminated. + # Make sure that the timeout is at least 2 seconds. + # FIXME So does this mean Irix systems are forced to always have + # FIXME a 2 second delay when calling read_nonblocking? That sucks. + if timeout is not None and timeout < 2: + timeout = 2 + + # Because of the select(0) check above, we know that no data + # is available right now. But if a non-zero timeout is given + # (possibly timeout=None), we call select() with a timeout. + if (timeout != 0) and select(timeout): + return super(spawn, self).read_nonblocking(size) + + if not self.isalive(): + # Some platforms, such as Irix, will claim that their + # processes are alive; timeout on the select; and + # then finally admit that they are not alive. + self.flag_eof = True + raise EOF('End of File (EOF). Very slow platform.') + else: + raise TIMEOUT('Timeout exceeded.') + + def write(self, s): + '''This is similar to send() except that there is no return value. + ''' + + self.send(s) + + def writelines(self, sequence): + '''This calls write() for each element in the sequence. The sequence + can be any iterable object producing strings, typically a list of + strings. This does not add line separators. There is no return value. + ''' + + for s in sequence: + self.write(s) + + def send(self, s): + '''Sends string ``s`` to the child process, returning the number of + bytes written. If a logfile is specified, a copy is written to that + log. + + The default terminal input mode is canonical processing unless set + otherwise by the child process. This allows backspace and other line + processing to be performed prior to transmitting to the receiving + program. As this is buffered, there is a limited size of such buffer. + + On Linux systems, this is 4096 (defined by N_TTY_BUF_SIZE). All + other systems honor the POSIX.1 definition PC_MAX_CANON -- 1024 + on OSX, 256 on OpenSolaris, and 1920 on FreeBSD. + + This value may be discovered using fpathconf(3):: + + >>> from os import fpathconf + >>> print(fpathconf(0, 'PC_MAX_CANON')) + 256 + + On such a system, only 256 bytes may be received per line. Any + subsequent bytes received will be discarded. BEL (``'\a'``) is then + sent to output if IMAXBEL (termios.h) is set by the tty driver. + This is usually enabled by default. Linux does not honor this as + an option -- it behaves as though it is always set on. + + Canonical input processing may be disabled altogether by executing + a shell, then stty(1), before executing the final program:: + + >>> bash = pexpect.spawn('/bin/bash', echo=False) + >>> bash.sendline('stty -icanon') + >>> bash.sendline('base64') + >>> bash.sendline('x' * 5000) + ''' + + if self.delaybeforesend is not None: + time.sleep(self.delaybeforesend) + + s = self._coerce_send_string(s) + self._log(s, 'send') + + b = self._encoder.encode(s, final=False) + return os.write(self.child_fd, b) + + def sendline(self, s=''): + '''Wraps send(), sending string ``s`` to child process, with + ``os.linesep`` automatically appended. Returns number of bytes + written. Only a limited number of bytes may be sent for each + line in the default terminal mode, see docstring of :meth:`send`. + ''' + s = self._coerce_send_string(s) + return self.send(s + self.linesep) + + def _log_control(self, s): + """Write control characters to the appropriate log files""" + if self.encoding is not None: + s = s.decode(self.encoding, 'replace') + self._log(s, 'send') + + def sendcontrol(self, char): + '''Helper method that wraps send() with mnemonic access for sending control + character to the child (such as Ctrl-C or Ctrl-D). For example, to send + Ctrl-G (ASCII 7, bell, '\a'):: + + child.sendcontrol('g') + + See also, sendintr() and sendeof(). + ''' + n, byte = self.ptyproc.sendcontrol(char) + self._log_control(byte) + return n + + def sendeof(self): + '''This sends an EOF to the child. This sends a character which causes + the pending parent output buffer to be sent to the waiting child + program without waiting for end-of-line. If it is the first character + of the line, the read() in the user program returns 0, which signifies + end-of-file. This means to work as expected a sendeof() has to be + called at the beginning of a line. This method does not send a newline. + It is the responsibility of the caller to ensure the eof is sent at the + beginning of a line. ''' + + n, byte = self.ptyproc.sendeof() + self._log_control(byte) + + def sendintr(self): + '''This sends a SIGINT to the child. It does not require + the SIGINT to be the first character on a line. ''' + + n, byte = self.ptyproc.sendintr() + self._log_control(byte) + + @property + def flag_eof(self): + return self.ptyproc.flag_eof + + @flag_eof.setter + def flag_eof(self, value): + self.ptyproc.flag_eof = value + + def eof(self): + '''This returns True if the EOF exception was ever raised. + ''' + return self.flag_eof + + def terminate(self, force=False): + '''This forces a child process to terminate. It starts nicely with + SIGHUP and SIGINT. If "force" is True then moves onto SIGKILL. This + returns True if the child was terminated. This returns False if the + child could not be terminated. ''' + + if not self.isalive(): + return True + try: + self.kill(signal.SIGHUP) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + self.kill(signal.SIGCONT) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + self.kill(signal.SIGINT) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + if force: + self.kill(signal.SIGKILL) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + else: + return False + return False + except OSError: + # I think there are kernel timing issues that sometimes cause + # this to happen. I think isalive() reports True, but the + # process is dead to the kernel. + # Make one last attempt to see if the kernel is up to date. + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + else: + return False + + def wait(self): + '''This waits until the child exits. This is a blocking call. This will + not read any data from the child, so this will block forever if the + child has unread output and has terminated. In other words, the child + may have printed output then called exit(), but, the child is + technically still alive until its output is read by the parent. + + This method is non-blocking if :meth:`wait` has already been called + previously or :meth:`isalive` method returns False. It simply returns + the previously determined exit status. + ''' + + ptyproc = self.ptyproc + with _wrap_ptyprocess_err(): + # exception may occur if "Is some other process attempting + # "job control with our child pid?" + exitstatus = ptyproc.wait() + self.status = ptyproc.status + self.exitstatus = ptyproc.exitstatus + self.signalstatus = ptyproc.signalstatus + self.terminated = True + + return exitstatus + + def isalive(self): + '''This tests if the child process is running or not. This is + non-blocking. If the child was terminated then this will read the + exitstatus or signalstatus of the child. This returns True if the child + process appears to be running or False if not. It can take literally + SECONDS for Solaris to return the right status. ''' + + ptyproc = self.ptyproc + with _wrap_ptyprocess_err(): + alive = ptyproc.isalive() + + if not alive: + self.status = ptyproc.status + self.exitstatus = ptyproc.exitstatus + self.signalstatus = ptyproc.signalstatus + self.terminated = True + + return alive + + def kill(self, sig): + + '''This sends the given signal to the child application. In keeping + with UNIX tradition it has a misleading name. It does not necessarily + kill the child unless you send the right signal. ''' + + # Same as os.kill, but the pid is given for you. + if self.isalive(): + os.kill(self.pid, sig) + + def getwinsize(self): + '''This returns the terminal window size of the child tty. The return + value is a tuple of (rows, cols). ''' + return self.ptyproc.getwinsize() + + def setwinsize(self, rows, cols): + '''This sets the terminal window size of the child tty. This will cause + a SIGWINCH signal to be sent to the child. This does not change the + physical window size. It changes the size reported to TTY-aware + applications like vi or curses -- applications that respond to the + SIGWINCH signal. ''' + return self.ptyproc.setwinsize(rows, cols) + + + def interact(self, escape_character=chr(29), + input_filter=None, output_filter=None): + + '''This gives control of the child process to the interactive user (the + human at the keyboard). Keystrokes are sent to the child process, and + the stdout and stderr output of the child process is printed. This + simply echos the child stdout and child stderr to the real stdout and + it echos the real stdin to the child stdin. When the user types the + escape_character this method will return None. The escape_character + will not be transmitted. The default for escape_character is + entered as ``Ctrl - ]``, the very same as BSD telnet. To prevent + escaping, escape_character may be set to None. + + If a logfile is specified, then the data sent and received from the + child process in interact mode is duplicated to the given log. + + You may pass in optional input and output filter functions. These + functions should take bytes array and return bytes array too. Even + with ``encoding='utf-8'`` support, meth:`interact` will always pass + input_filter and output_filter bytes. You may need to wrap your + function to decode and encode back to UTF-8. + + The output_filter will be passed all the output from the child process. + The input_filter will be passed all the keyboard input from the user. + The input_filter is run BEFORE the check for the escape_character. + + Note that if you change the window size of the parent the SIGWINCH + signal will not be passed through to the child. If you want the child + window size to change when the parent's window size changes then do + something like the following example:: + + import pexpect, struct, fcntl, termios, signal, sys + def sigwinch_passthrough (sig, data): + s = struct.pack("HHHH", 0, 0, 0, 0) + a = struct.unpack('hhhh', fcntl.ioctl(sys.stdout.fileno(), + termios.TIOCGWINSZ , s)) + if not p.closed: + p.setwinsize(a[0],a[1]) + + # Note this 'p' is global and used in sigwinch_passthrough. + p = pexpect.spawn('/bin/bash') + signal.signal(signal.SIGWINCH, sigwinch_passthrough) + p.interact() + ''' + + # Flush the buffer. + self.write_to_stdout(self.buffer) + self.stdout.flush() + self._buffer = self.buffer_type() + mode = tty.tcgetattr(self.STDIN_FILENO) + tty.setraw(self.STDIN_FILENO) + if escape_character is not None and PY3: + escape_character = escape_character.encode('latin-1') + try: + self.__interact_copy(escape_character, input_filter, output_filter) + finally: + tty.tcsetattr(self.STDIN_FILENO, tty.TCSAFLUSH, mode) + + def __interact_writen(self, fd, data): + '''This is used by the interact() method. + ''' + + while data != b'' and self.isalive(): + n = os.write(fd, data) + data = data[n:] + + def __interact_read(self, fd): + '''This is used by the interact() method. + ''' + + return os.read(fd, 1000) + + def __interact_copy( + self, escape_character=None, input_filter=None, output_filter=None + ): + + '''This is used by the interact() method. + ''' + + while self.isalive(): + if self.use_poll: + r = poll_ignore_interrupts([self.child_fd, self.STDIN_FILENO]) + else: + r, w, e = select_ignore_interrupts( + [self.child_fd, self.STDIN_FILENO], [], [] + ) + if self.child_fd in r: + try: + data = self.__interact_read(self.child_fd) + except OSError as err: + if err.args[0] == errno.EIO: + # Linux-style EOF + break + raise + if data == b'': + # BSD-style EOF + break + if output_filter: + data = output_filter(data) + self._log(data, 'read') + os.write(self.STDOUT_FILENO, data) + if self.STDIN_FILENO in r: + data = self.__interact_read(self.STDIN_FILENO) + if input_filter: + data = input_filter(data) + i = -1 + if escape_character is not None: + i = data.rfind(escape_character) + if i != -1: + data = data[:i] + if data: + self._log(data, 'send') + self.__interact_writen(self.child_fd, data) + break + self._log(data, 'send') + self.__interact_writen(self.child_fd, data) + + +def spawnu(*args, **kwargs): + """Deprecated: pass encoding to spawn() instead.""" + kwargs.setdefault('encoding', 'utf-8') + return spawn(*args, **kwargs) diff --git a/class/pexpect/pxssh.py b/class/pexpect/pxssh.py new file mode 100644 index 00000000..3d53bd97 --- /dev/null +++ b/class/pexpect/pxssh.py @@ -0,0 +1,537 @@ +'''This class extends pexpect.spawn to specialize setting up SSH connections. +This adds methods for login, logout, and expecting the shell prompt. + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +from pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn +import time +import os +import sys +import re + +__all__ = ['ExceptionPxssh', 'pxssh'] + +# Exception classes used by this module. +class ExceptionPxssh(ExceptionPexpect): + '''Raised for pxssh exceptions. + ''' + +if sys.version_info > (3, 0): + from shlex import quote +else: + _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search + + def quote(s): + """Return a shell-escaped version of the string *s*.""" + if not s: + return "''" + if _find_unsafe(s) is None: + return s + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + s.replace("'", "'\"'\"'") + "'" + +class pxssh (spawn): + '''This class extends pexpect.spawn to specialize setting up SSH + connections. This adds methods for login, logout, and expecting the shell + prompt. It does various tricky things to handle many situations in the SSH + login process. For example, if the session is your first login, then pxssh + automatically accepts the remote certificate; or if you have public key + authentication setup then pxssh won't wait for the password prompt. + + pxssh uses the shell prompt to synchronize output from the remote host. In + order to make this more robust it sets the shell prompt to something more + unique than just $ or #. This should work on most Borne/Bash or Csh style + shells. + + Example that runs a few commands on a remote server and prints the result:: + + from pexpect import pxssh + import getpass + try: + s = pxssh.pxssh() + hostname = raw_input('hostname: ') + username = raw_input('username: ') + password = getpass.getpass('password: ') + s.login(hostname, username, password) + s.sendline('uptime') # run a command + s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.sendline('ls -l') + s.prompt() + print(s.before) + s.sendline('df') + s.prompt() + print(s.before) + s.logout() + except pxssh.ExceptionPxssh as e: + print("pxssh failed on login.") + print(e) + + Example showing how to specify SSH options:: + + from pexpect import pxssh + s = pxssh.pxssh(options={ + "StrictHostKeyChecking": "no", + "UserKnownHostsFile": "/dev/null"}) + ... + + Note that if you have ssh-agent running while doing development with pxssh + then this can lead to a lot of confusion. Many X display managers (xdm, + gdm, kdm, etc.) will automatically start a GUI agent. You may see a GUI + dialog box popup asking for a password during development. You should turn + off any key agents during testing. The 'force_password' attribute will turn + off public key authentication. This will only work if the remote SSH server + is configured to allow password logins. Example of using 'force_password' + attribute:: + + s = pxssh.pxssh() + s.force_password = True + hostname = raw_input('hostname: ') + username = raw_input('username: ') + password = getpass.getpass('password: ') + s.login (hostname, username, password) + + `debug_command_string` is only for the test suite to confirm that the string + generated for SSH is correct, using this will not allow you to do + anything other than get a string back from `pxssh.pxssh.login()`. + ''' + + def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None, + logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True, + options={}, encoding=None, codec_errors='strict', + debug_command_string=False, use_poll=False): + + spawn.__init__(self, None, timeout=timeout, maxread=maxread, + searchwindowsize=searchwindowsize, logfile=logfile, + cwd=cwd, env=env, ignore_sighup=ignore_sighup, echo=echo, + encoding=encoding, codec_errors=codec_errors, use_poll=use_poll) + + self.name = '' + + #SUBTLE HACK ALERT! Note that the command that SETS the prompt uses a + #slightly different string than the regular expression to match it. This + #is because when you set the prompt the command will echo back, but we + #don't want to match the echoed command. So if we make the set command + #slightly different than the regex we eliminate the problem. To make the + #set command different we add a backslash in front of $. The $ doesn't + #need to be escaped, but it doesn't hurt and serves to make the set + #prompt command different than the regex. + + # used to match the command-line prompt + self.UNIQUE_PROMPT = r"\[PEXPECT\][\$\#] " + self.PROMPT = self.UNIQUE_PROMPT + + # used to set shell command-line prompt to UNIQUE_PROMPT. + self.PROMPT_SET_SH = r"PS1='[PEXPECT]\$ '" + self.PROMPT_SET_CSH = r"set prompt='[PEXPECT]\$ '" + self.SSH_OPTS = ("-o'RSAAuthentication=no'" + + " -o 'PubkeyAuthentication=no'") +# Disabling host key checking, makes you vulnerable to MITM attacks. +# + " -o 'StrictHostKeyChecking=no'" +# + " -o 'UserKnownHostsFile /dev/null' ") + # Disabling X11 forwarding gets rid of the annoying SSH_ASKPASS from + # displaying a GUI password dialog. I have not figured out how to + # disable only SSH_ASKPASS without also disabling X11 forwarding. + # Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying! + #self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'" + self.force_password = False + + self.debug_command_string = debug_command_string + + # User defined SSH options, eg, + # ssh.otions = dict(StrictHostKeyChecking="no",UserKnownHostsFile="/dev/null") + self.options = options + + def levenshtein_distance(self, a, b): + '''This calculates the Levenshtein distance between a and b. + ''' + + n, m = len(a), len(b) + if n > m: + a,b = b,a + n,m = m,n + current = range(n+1) + for i in range(1,m+1): + previous, current = current, [i]+[0]*n + for j in range(1,n+1): + add, delete = previous[j]+1, current[j-1]+1 + change = previous[j-1] + if a[j-1] != b[i-1]: + change = change + 1 + current[j] = min(add, delete, change) + return current[n] + + def try_read_prompt(self, timeout_multiplier): + '''This facilitates using communication timeouts to perform + synchronization as quickly as possible, while supporting high latency + connections with a tunable worst case performance. Fast connections + should be read almost immediately. Worst case performance for this + method is timeout_multiplier * 3 seconds. + ''' + + # maximum time allowed to read the first response + first_char_timeout = timeout_multiplier * 0.5 + + # maximum time allowed between subsequent characters + inter_char_timeout = timeout_multiplier * 0.1 + + # maximum time for reading the entire prompt + total_timeout = timeout_multiplier * 3.0 + + prompt = self.string_type() + begin = time.time() + expired = 0.0 + timeout = first_char_timeout + + while expired < total_timeout: + try: + prompt += self.read_nonblocking(size=1, timeout=timeout) + expired = time.time() - begin # updated total time expired + timeout = inter_char_timeout + except TIMEOUT: + break + + return prompt + + def sync_original_prompt (self, sync_multiplier=1.0): + '''This attempts to find the prompt. Basically, press enter and record + the response; press enter again and record the response; if the two + responses are similar then assume we are at the original prompt. + This can be a slow function. Worst case with the default sync_multiplier + can take 12 seconds. Low latency connections are more likely to fail + with a low sync_multiplier. Best case sync time gets worse with a + high sync multiplier (500 ms with default). ''' + + # All of these timing pace values are magic. + # I came up with these based on what seemed reliable for + # connecting to a heavily loaded machine I have. + self.sendline() + time.sleep(0.1) + + try: + # Clear the buffer before getting the prompt. + self.try_read_prompt(sync_multiplier) + except TIMEOUT: + pass + + self.sendline() + x = self.try_read_prompt(sync_multiplier) + + self.sendline() + a = self.try_read_prompt(sync_multiplier) + + self.sendline() + b = self.try_read_prompt(sync_multiplier) + + ld = self.levenshtein_distance(a,b) + len_a = len(a) + if len_a == 0: + return False + if float(ld)/len_a < 0.4: + return True + return False + + ### TODO: This is getting messy and I'm pretty sure this isn't perfect. + ### TODO: I need to draw a flow chart for this. + ### TODO: Unit tests for SSH tunnels, remote SSH command exec, disabling original prompt sync + def login (self, server, username=None, password='', terminal_type='ansi', + original_prompt=r"[#$]", login_timeout=10, port=None, + auto_prompt_reset=True, ssh_key=None, quiet=True, + sync_multiplier=1, check_local_ip=True, + password_regex=r'(?i)(?:password:)|(?:passphrase for key)', + ssh_tunnels={}, spawn_local_ssh=True, + sync_original_prompt=True, ssh_config=None, cmd='ssh'): + '''This logs the user into the given server. + + It uses 'original_prompt' to try to find the prompt right after login. + When it finds the prompt it immediately tries to reset the prompt to + something more easily matched. The default 'original_prompt' is very + optimistic and is easily fooled. It's more reliable to try to match the original + prompt as exactly as possible to prevent false matches by server + strings such as the "Message Of The Day". On many systems you can + disable the MOTD on the remote server by creating a zero-length file + called :file:`~/.hushlogin` on the remote server. If a prompt cannot be found + then this will not necessarily cause the login to fail. In the case of + a timeout when looking for the prompt we assume that the original + prompt was so weird that we could not match it, so we use a few tricks + to guess when we have reached the prompt. Then we hope for the best and + blindly try to reset the prompt to something more unique. If that fails + then login() raises an :class:`ExceptionPxssh` exception. + + In some situations it is not possible or desirable to reset the + original prompt. In this case, pass ``auto_prompt_reset=False`` to + inhibit setting the prompt to the UNIQUE_PROMPT. Remember that pxssh + uses a unique prompt in the :meth:`prompt` method. If the original prompt is + not reset then this will disable the :meth:`prompt` method unless you + manually set the :attr:`PROMPT` attribute. + + Set ``password_regex`` if there is a MOTD message with `password` in it. + Changing this is like playing in traffic, don't (p)expect it to match straight + away. + + If you require to connect to another SSH server from the your original SSH + connection set ``spawn_local_ssh`` to `False` and this will use your current + session to do so. Setting this option to `False` and not having an active session + will trigger an error. + + Set ``ssh_key`` to a file path to an SSH private key to use that SSH key + for the session authentication. + Set ``ssh_key`` to `True` to force passing the current SSH authentication socket + to the desired ``hostname``. + + Set ``ssh_config`` to a file path string of an SSH client config file to pass that + file to the client to handle itself. You may set any options you wish in here, however + doing so will require you to post extra information that you may not want to if you + run into issues. + + Alter the ``cmd`` to change the ssh client used, or to prepend it with network + namespaces. For example ```cmd="ip netns exec vlan2 ssh"``` to execute the ssh in + network namespace named ```vlan```. + ''' + + session_regex_array = ["(?i)are you sure you want to continue connecting", original_prompt, password_regex, "(?i)permission denied", "(?i)terminal type", TIMEOUT] + session_init_regex_array = [] + session_init_regex_array.extend(session_regex_array) + session_init_regex_array.extend(["(?i)connection closed by remote host", EOF]) + + ssh_options = ''.join([" -o '%s=%s'" % (o, v) for (o, v) in self.options.items()]) + if quiet: + ssh_options = ssh_options + ' -q' + if not check_local_ip: + ssh_options = ssh_options + " -o'NoHostAuthenticationForLocalhost=yes'" + if self.force_password: + ssh_options = ssh_options + ' ' + self.SSH_OPTS + if ssh_config is not None: + if spawn_local_ssh and not os.path.isfile(ssh_config): + raise ExceptionPxssh('SSH config does not exist or is not a file.') + ssh_options = ssh_options + ' -F ' + ssh_config + if port is not None: + ssh_options = ssh_options + ' -p %s'%(str(port)) + if ssh_key is not None: + # Allow forwarding our SSH key to the current session + if ssh_key==True: + ssh_options = ssh_options + ' -A' + else: + if spawn_local_ssh and not os.path.isfile(ssh_key): + raise ExceptionPxssh('private ssh key does not exist or is not a file.') + ssh_options = ssh_options + ' -i %s' % (ssh_key) + + # SSH tunnels, make sure you know what you're putting into the lists + # under each heading. Do not expect these to open 100% of the time, + # The port you're requesting might be bound. + # + # The structure should be like this: + # { 'local': ['2424:localhost:22'], # Local SSH tunnels + # 'remote': ['2525:localhost:22'], # Remote SSH tunnels + # 'dynamic': [8888] } # Dynamic/SOCKS tunnels + if ssh_tunnels!={} and isinstance({},type(ssh_tunnels)): + tunnel_types = { + 'local':'L', + 'remote':'R', + 'dynamic':'D' + } + for tunnel_type in tunnel_types: + cmd_type = tunnel_types[tunnel_type] + if tunnel_type in ssh_tunnels: + tunnels = ssh_tunnels[tunnel_type] + for tunnel in tunnels: + if spawn_local_ssh==False: + tunnel = quote(str(tunnel)) + ssh_options = ssh_options + ' -' + cmd_type + ' ' + str(tunnel) + + if username is not None: + ssh_options = ssh_options + ' -l ' + username + elif ssh_config is None: + raise TypeError('login() needs either a username or an ssh_config') + else: # make sure ssh_config has an entry for the server with a username + with open(ssh_config, 'rt') as f: + lines = [l.strip() for l in f.readlines()] + + server_regex = r'^Host\s+%s\s*$' % server + user_regex = r'^User\s+\w+\s*$' + config_has_server = False + server_has_username = False + for line in lines: + if not config_has_server and re.match(server_regex, line, re.IGNORECASE): + config_has_server = True + elif config_has_server and 'hostname' in line.lower(): + pass + elif config_has_server and 'host' in line.lower(): + server_has_username = False # insurance + break # we have left the relevant section + elif config_has_server and re.match(user_regex, line, re.IGNORECASE): + server_has_username = True + break + + if lines: + del line + + del lines + + if not config_has_server: + raise TypeError('login() ssh_config has no Host entry for %s' % server) + elif not server_has_username: + raise TypeError('login() ssh_config has no user entry for %s' % server) + + cmd += " %s %s" % (ssh_options, server) + if self.debug_command_string: + return(cmd) + + # Are we asking for a local ssh command or to spawn one in another session? + if spawn_local_ssh: + spawn._spawn(self, cmd) + else: + self.sendline(cmd) + + # This does not distinguish between a remote server 'password' prompt + # and a local ssh 'passphrase' prompt (for unlocking a private key). + i = self.expect(session_init_regex_array, timeout=login_timeout) + + # First phase + if i==0: + # New certificate -- always accept it. + # This is what you get if SSH does not have the remote host's + # public key stored in the 'known_hosts' cache. + self.sendline("yes") + i = self.expect(session_regex_array) + if i==2: # password or passphrase + self.sendline(password) + i = self.expect(session_regex_array) + if i==4: + self.sendline(terminal_type) + i = self.expect(session_regex_array) + if i==7: + self.close() + raise ExceptionPxssh('Could not establish connection to host') + + # Second phase + if i==0: + # This is weird. This should not happen twice in a row. + self.close() + raise ExceptionPxssh('Weird error. Got "are you sure" prompt twice.') + elif i==1: # can occur if you have a public key pair set to authenticate. + ### TODO: May NOT be OK if expect() got tricked and matched a false prompt. + pass + elif i==2: # password prompt again + # For incorrect passwords, some ssh servers will + # ask for the password again, others return 'denied' right away. + # If we get the password prompt again then this means + # we didn't get the password right the first time. + self.close() + raise ExceptionPxssh('password refused') + elif i==3: # permission denied -- password was bad. + self.close() + raise ExceptionPxssh('permission denied') + elif i==4: # terminal type again? WTF? + self.close() + raise ExceptionPxssh('Weird error. Got "terminal type" prompt twice.') + elif i==5: # Timeout + #This is tricky... I presume that we are at the command-line prompt. + #It may be that the shell prompt was so weird that we couldn't match + #it. Or it may be that we couldn't log in for some other reason. I + #can't be sure, but it's safe to guess that we did login because if + #I presume wrong and we are not logged in then this should be caught + #later when I try to set the shell prompt. + pass + elif i==6: # Connection closed by remote host + self.close() + raise ExceptionPxssh('connection closed') + else: # Unexpected + self.close() + raise ExceptionPxssh('unexpected login response') + if sync_original_prompt: + if not self.sync_original_prompt(sync_multiplier): + self.close() + raise ExceptionPxssh('could not synchronize with original prompt') + # We appear to be in. + # set shell prompt to something unique. + if auto_prompt_reset: + if not self.set_unique_prompt(): + self.close() + raise ExceptionPxssh('could not set shell prompt ' + '(received: %r, expected: %r).' % ( + self.before, self.PROMPT,)) + return True + + def logout (self): + '''Sends exit to the remote shell. + + If there are stopped jobs then this automatically sends exit twice. + ''' + self.sendline("exit") + index = self.expect([EOF, "(?i)there are stopped jobs"]) + if index==1: + self.sendline("exit") + self.expect(EOF) + self.close() + + def prompt(self, timeout=-1): + '''Match the next shell prompt. + + This is little more than a short-cut to the :meth:`~pexpect.spawn.expect` + method. Note that if you called :meth:`login` with + ``auto_prompt_reset=False``, then before calling :meth:`prompt` you must + set the :attr:`PROMPT` attribute to a regex that it will use for + matching the prompt. + + Calling :meth:`prompt` will erase the contents of the :attr:`before` + attribute even if no prompt is ever matched. If timeout is not given or + it is set to -1 then self.timeout is used. + + :return: True if the shell prompt was matched, False if the timeout was + reached. + ''' + + if timeout == -1: + timeout = self.timeout + i = self.expect([self.PROMPT, TIMEOUT], timeout=timeout) + if i==1: + return False + return True + + def set_unique_prompt(self): + '''This sets the remote prompt to something more unique than ``#`` or ``$``. + This makes it easier for the :meth:`prompt` method to match the shell prompt + unambiguously. This method is called automatically by the :meth:`login` + method, but you may want to call it manually if you somehow reset the + shell prompt. For example, if you 'su' to a different user then you + will need to manually reset the prompt. This sends shell commands to + the remote host to set the prompt, so this assumes the remote host is + ready to receive commands. + + Alternatively, you may use your own prompt pattern. In this case you + should call :meth:`login` with ``auto_prompt_reset=False``; then set the + :attr:`PROMPT` attribute to a regular expression. After that, the + :meth:`prompt` method will try to match your prompt pattern. + ''' + + self.sendline("unset PROMPT_COMMAND") + self.sendline(self.PROMPT_SET_SH) # sh-style + i = self.expect ([TIMEOUT, self.PROMPT], timeout=10) + if i == 0: # csh-style + self.sendline(self.PROMPT_SET_CSH) + i = self.expect([TIMEOUT, self.PROMPT], timeout=10) + if i == 0: + return False + return True + +# vi:ts=4:sw=4:expandtab:ft=python: diff --git a/class/pexpect/replwrap.py b/class/pexpect/replwrap.py new file mode 100644 index 00000000..6c34ce41 --- /dev/null +++ b/class/pexpect/replwrap.py @@ -0,0 +1,130 @@ +"""Generic wrapper for read-eval-print-loops, a.k.a. interactive shells +""" +import os.path +import signal +import sys + +import pexpect + +PY3 = (sys.version_info[0] >= 3) + +if PY3: + basestring = str + +PEXPECT_PROMPT = u'[PEXPECT_PROMPT>' +PEXPECT_CONTINUATION_PROMPT = u'[PEXPECT_PROMPT+' + +class REPLWrapper(object): + """Wrapper for a REPL. + + :param cmd_or_spawn: This can either be an instance of :class:`pexpect.spawn` + in which a REPL has already been started, or a str command to start a new + REPL process. + :param str orig_prompt: The prompt to expect at first. + :param str prompt_change: A command to change the prompt to something more + unique. If this is ``None``, the prompt will not be changed. This will + be formatted with the new and continuation prompts as positional + parameters, so you can use ``{}`` style formatting to insert them into + the command. + :param str new_prompt: The more unique prompt to expect after the change. + :param str extra_init_cmd: Commands to do extra initialisation, such as + disabling pagers. + """ + def __init__(self, cmd_or_spawn, orig_prompt, prompt_change, + new_prompt=PEXPECT_PROMPT, + continuation_prompt=PEXPECT_CONTINUATION_PROMPT, + extra_init_cmd=None): + if isinstance(cmd_or_spawn, basestring): + self.child = pexpect.spawn(cmd_or_spawn, echo=False, encoding='utf-8') + else: + self.child = cmd_or_spawn + if self.child.echo: + # Existing spawn instance has echo enabled, disable it + # to prevent our input from being repeated to output. + self.child.setecho(False) + self.child.waitnoecho() + + if prompt_change is None: + self.prompt = orig_prompt + else: + self.set_prompt(orig_prompt, + prompt_change.format(new_prompt, continuation_prompt)) + self.prompt = new_prompt + self.continuation_prompt = continuation_prompt + + self._expect_prompt() + + if extra_init_cmd is not None: + self.run_command(extra_init_cmd) + + def set_prompt(self, orig_prompt, prompt_change): + self.child.expect(orig_prompt) + self.child.sendline(prompt_change) + + def _expect_prompt(self, timeout=-1, async_=False): + return self.child.expect_exact([self.prompt, self.continuation_prompt], + timeout=timeout, async_=async_) + + def run_command(self, command, timeout=-1, async_=False): + """Send a command to the REPL, wait for and return output. + + :param str command: The command to send. Trailing newlines are not needed. + This should be a complete block of input that will trigger execution; + if a continuation prompt is found after sending input, :exc:`ValueError` + will be raised. + :param int timeout: How long to wait for the next prompt. -1 means the + default from the :class:`pexpect.spawn` object (default 30 seconds). + None means to wait indefinitely. + :param bool async_: On Python 3.4, or Python 3.3 with asyncio + installed, passing ``async_=True`` will make this return an + :mod:`asyncio` Future, which you can yield from to get the same + result that this method would normally give directly. + """ + # Split up multiline commands and feed them in bit-by-bit + cmdlines = command.splitlines() + # splitlines ignores trailing newlines - add it back in manually + if command.endswith('\n'): + cmdlines.append('') + if not cmdlines: + raise ValueError("No command was given") + + if async_: + from ._async import repl_run_command_async + return repl_run_command_async(self, cmdlines, timeout) + + res = [] + self.child.sendline(cmdlines[0]) + for line in cmdlines[1:]: + self._expect_prompt(timeout=timeout) + res.append(self.child.before) + self.child.sendline(line) + + # Command was fully submitted, now wait for the next prompt + if self._expect_prompt(timeout=timeout) == 1: + # We got the continuation prompt - command was incomplete + self.child.kill(signal.SIGINT) + self._expect_prompt(timeout=1) + raise ValueError("Continuation prompt found - input was incomplete:\n" + + command) + return u''.join(res + [self.child.before]) + +def python(command=sys.executable): + """Start a Python shell and return a :class:`REPLWrapper` object.""" + return REPLWrapper(command, u">>> ", u"import sys; sys.ps1={0!r}; sys.ps2={1!r}") + +def bash(command="bash"): + """Start a bash shell and return a :class:`REPLWrapper` object.""" + bashrc = os.path.join(os.path.dirname(__file__), 'bashrc.sh') + child = pexpect.spawn(command, ['--rcfile', bashrc], echo=False, + encoding='utf-8') + + # If the user runs 'env', the value of PS1 will be in the output. To avoid + # replwrap seeing that as the next prompt, we'll embed the marker characters + # for invisible characters in the prompt; these show up when inspecting the + # environment variable, but not when bash displays the prompt. + ps1 = PEXPECT_PROMPT[:5] + u'\\[\\]' + PEXPECT_PROMPT[5:] + ps2 = PEXPECT_CONTINUATION_PROMPT[:5] + u'\\[\\]' + PEXPECT_CONTINUATION_PROMPT[5:] + prompt_change = u"PS1='{0}' PS2='{1}' PROMPT_COMMAND=''".format(ps1, ps2) + + return REPLWrapper(child, u'\\$', prompt_change, + extra_init_cmd="export PAGER=cat") diff --git a/class/pexpect/run.py b/class/pexpect/run.py new file mode 100644 index 00000000..5695ab7f --- /dev/null +++ b/class/pexpect/run.py @@ -0,0 +1,157 @@ +import sys +import types + +from .exceptions import EOF, TIMEOUT +from .pty_spawn import spawn + +def run(command, timeout=30, withexitstatus=False, events=None, + extra_args=None, logfile=None, cwd=None, env=None, **kwargs): + + ''' + This function runs the given command; waits for it to finish; then + returns all output as a string. STDERR is included in output. If the full + path to the command is not given then the path is searched. + + Note that lines are terminated by CR/LF (\\r\\n) combination even on + UNIX-like systems because this is the standard for pseudottys. If you set + 'withexitstatus' to true, then run will return a tuple of (command_output, + exitstatus). If 'withexitstatus' is false then this returns just + command_output. + + The run() function can often be used instead of creating a spawn instance. + For example, the following code uses spawn:: + + from pexpect import * + child = spawn('scp foo user@example.com:.') + child.expect('(?i)password') + child.sendline(mypassword) + + The previous code can be replace with the following:: + + from pexpect import * + run('scp foo user@example.com:.', events={'(?i)password': mypassword}) + + **Examples** + + Start the apache daemon on the local machine:: + + from pexpect import * + run("/usr/local/apache/bin/apachectl start") + + Check in a file using SVN:: + + from pexpect import * + run("svn ci -m 'automatic commit' my_file.py") + + Run a command and capture exit status:: + + from pexpect import * + (command_output, exitstatus) = run('ls -l /bin', withexitstatus=1) + + The following will run SSH and execute 'ls -l' on the remote machine. The + password 'secret' will be sent if the '(?i)password' pattern is ever seen:: + + run("ssh username@machine.example.com 'ls -l'", + events={'(?i)password':'secret\\n'}) + + This will start mencoder to rip a video from DVD. This will also display + progress ticks every 5 seconds as it runs. For example:: + + from pexpect import * + def print_ticks(d): + print d['event_count'], + run("mencoder dvd://1 -o video.avi -oac copy -ovc copy", + events={TIMEOUT:print_ticks}, timeout=5) + + The 'events' argument should be either a dictionary or a tuple list that + contains patterns and responses. Whenever one of the patterns is seen + in the command output, run() will send the associated response string. + So, run() in the above example can be also written as:: + + run("mencoder dvd://1 -o video.avi -oac copy -ovc copy", + events=[(TIMEOUT,print_ticks)], timeout=5) + + Use a tuple list for events if the command output requires a delicate + control over what pattern should be matched, since the tuple list is passed + to pexpect() as its pattern list, with the order of patterns preserved. + + Note that you should put newlines in your string if Enter is necessary. + + Like the example above, the responses may also contain a callback, either + a function or method. It should accept a dictionary value as an argument. + The dictionary contains all the locals from the run() function, so you can + access the child spawn object or any other variable defined in run() + (event_count, child, and extra_args are the most useful). A callback may + return True to stop the current run process. Otherwise run() continues + until the next event. A callback may also return a string which will be + sent to the child. 'extra_args' is not used by directly run(). It provides + a way to pass data to a callback function through run() through the locals + dictionary passed to a callback. + + Like :class:`spawn`, passing *encoding* will make it work with unicode + instead of bytes. You can pass *codec_errors* to control how errors in + encoding and decoding are handled. + ''' + if timeout == -1: + child = spawn(command, maxread=2000, logfile=logfile, cwd=cwd, env=env, + **kwargs) + else: + child = spawn(command, timeout=timeout, maxread=2000, logfile=logfile, + cwd=cwd, env=env, **kwargs) + if isinstance(events, list): + patterns= [x for x,y in events] + responses = [y for x,y in events] + elif isinstance(events, dict): + patterns = list(events.keys()) + responses = list(events.values()) + else: + # This assumes EOF or TIMEOUT will eventually cause run to terminate. + patterns = None + responses = None + child_result_list = [] + event_count = 0 + while True: + try: + index = child.expect(patterns) + if isinstance(child.after, child.allowed_string_types): + child_result_list.append(child.before + child.after) + else: + # child.after may have been a TIMEOUT or EOF, + # which we don't want appended to the list. + child_result_list.append(child.before) + if isinstance(responses[index], child.allowed_string_types): + child.send(responses[index]) + elif (isinstance(responses[index], types.FunctionType) or + isinstance(responses[index], types.MethodType)): + callback_result = responses[index](locals()) + sys.stdout.flush() + if isinstance(callback_result, child.allowed_string_types): + child.send(callback_result) + elif callback_result: + break + else: + raise TypeError("parameter `event' at index {index} must be " + "a string, method, or function: {value!r}" + .format(index=index, value=responses[index])) + event_count = event_count + 1 + except TIMEOUT: + child_result_list.append(child.before) + break + except EOF: + child_result_list.append(child.before) + break + child_result = child.string_type().join(child_result_list) + if withexitstatus: + child.close() + return (child_result, child.exitstatus) + else: + return child_result + +def runu(command, timeout=30, withexitstatus=False, events=None, + extra_args=None, logfile=None, cwd=None, env=None, **kwargs): + """Deprecated: pass encoding to run() instead. + """ + kwargs.setdefault('encoding', 'utf-8') + return run(command, timeout=timeout, withexitstatus=withexitstatus, + events=events, extra_args=extra_args, logfile=logfile, cwd=cwd, + env=env, **kwargs) diff --git a/class/pexpect/screen.py b/class/pexpect/screen.py new file mode 100644 index 00000000..79f95c4e --- /dev/null +++ b/class/pexpect/screen.py @@ -0,0 +1,431 @@ +'''This implements a virtual screen. This is used to support ANSI terminal +emulation. The screen representation and state is implemented in this class. +Most of the methods are inspired by ANSI screen control codes. The +:class:`~pexpect.ANSI.ANSI` class extends this class to add parsing of ANSI +escape codes. + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +import codecs +import copy +import sys + +import warnings + +warnings.warn(("pexpect.screen and pexpect.ANSI are deprecated. " + "We recommend using pyte to emulate a terminal screen: " + "https://pypi.python.org/pypi/pyte"), + stacklevel=2) + +NUL = 0 # Fill character; ignored on input. +ENQ = 5 # Transmit answerback message. +BEL = 7 # Ring the bell. +BS = 8 # Move cursor left. +HT = 9 # Move cursor to next tab stop. +LF = 10 # Line feed. +VT = 11 # Same as LF. +FF = 12 # Same as LF. +CR = 13 # Move cursor to left margin or newline. +SO = 14 # Invoke G1 character set. +SI = 15 # Invoke G0 character set. +XON = 17 # Resume transmission. +XOFF = 19 # Halt transmission. +CAN = 24 # Cancel escape sequence. +SUB = 26 # Same as CAN. +ESC = 27 # Introduce a control sequence. +DEL = 127 # Fill character; ignored on input. +SPACE = u' ' # Space or blank character. + +PY3 = (sys.version_info[0] >= 3) +if PY3: + unicode = str + +def constrain (n, min, max): + + '''This returns a number, n constrained to the min and max bounds. ''' + + if n < min: + return min + if n > max: + return max + return n + +class screen: + '''This object maintains the state of a virtual text screen as a + rectangular array. This maintains a virtual cursor position and handles + scrolling as characters are added. This supports most of the methods needed + by an ANSI text screen. Row and column indexes are 1-based (not zero-based, + like arrays). + + Characters are represented internally using unicode. Methods that accept + input characters, when passed 'bytes' (which in Python 2 is equivalent to + 'str'), convert them from the encoding specified in the 'encoding' + parameter to the constructor. Methods that return screen contents return + unicode strings, with the exception of __str__() under Python 2. Passing + ``encoding=None`` limits the API to only accept unicode input, so passing + bytes in will raise :exc:`TypeError`. + ''' + def __init__(self, r=24, c=80, encoding='latin-1', encoding_errors='replace'): + '''This initializes a blank screen of the given dimensions.''' + + self.rows = r + self.cols = c + self.encoding = encoding + self.encoding_errors = encoding_errors + if encoding is not None: + self.decoder = codecs.getincrementaldecoder(encoding)(encoding_errors) + else: + self.decoder = None + self.cur_r = 1 + self.cur_c = 1 + self.cur_saved_r = 1 + self.cur_saved_c = 1 + self.scroll_row_start = 1 + self.scroll_row_end = self.rows + self.w = [ [SPACE] * self.cols for _ in range(self.rows)] + + def _decode(self, s): + '''This converts from the external coding system (as passed to + the constructor) to the internal one (unicode). ''' + if self.decoder is not None: + return self.decoder.decode(s) + else: + raise TypeError("This screen was constructed with encoding=None, " + "so it does not handle bytes.") + + def _unicode(self): + '''This returns a printable representation of the screen as a unicode + string (which, under Python 3.x, is the same as 'str'). The end of each + screen line is terminated by a newline.''' + + return u'\n'.join ([ u''.join(c) for c in self.w ]) + + if PY3: + __str__ = _unicode + else: + __unicode__ = _unicode + + def __str__(self): + '''This returns a printable representation of the screen. The end of + each screen line is terminated by a newline. ''' + encoding = self.encoding or 'ascii' + return self._unicode().encode(encoding, 'replace') + + def dump (self): + '''This returns a copy of the screen as a unicode string. This is similar to + __str__/__unicode__ except that lines are not terminated with line + feeds.''' + + return u''.join ([ u''.join(c) for c in self.w ]) + + def pretty (self): + '''This returns a copy of the screen as a unicode string with an ASCII + text box around the screen border. This is similar to + __str__/__unicode__ except that it adds a box.''' + + top_bot = u'+' + u'-'*self.cols + u'+\n' + return top_bot + u'\n'.join([u'|'+line+u'|' for line in unicode(self).split(u'\n')]) + u'\n' + top_bot + + def fill (self, ch=SPACE): + + if isinstance(ch, bytes): + ch = self._decode(ch) + + self.fill_region (1,1,self.rows,self.cols, ch) + + def fill_region (self, rs,cs, re,ce, ch=SPACE): + + if isinstance(ch, bytes): + ch = self._decode(ch) + + rs = constrain (rs, 1, self.rows) + re = constrain (re, 1, self.rows) + cs = constrain (cs, 1, self.cols) + ce = constrain (ce, 1, self.cols) + if rs > re: + rs, re = re, rs + if cs > ce: + cs, ce = ce, cs + for r in range (rs, re+1): + for c in range (cs, ce + 1): + self.put_abs (r,c,ch) + + def cr (self): + '''This moves the cursor to the beginning (col 1) of the current row. + ''' + + self.cursor_home (self.cur_r, 1) + + def lf (self): + '''This moves the cursor down with scrolling. + ''' + + old_r = self.cur_r + self.cursor_down() + if old_r == self.cur_r: + self.scroll_up () + self.erase_line() + + def crlf (self): + '''This advances the cursor with CRLF properties. + The cursor will line wrap and the screen may scroll. + ''' + + self.cr () + self.lf () + + def newline (self): + '''This is an alias for crlf(). + ''' + + self.crlf() + + def put_abs (self, r, c, ch): + '''Screen array starts at 1 index.''' + + r = constrain (r, 1, self.rows) + c = constrain (c, 1, self.cols) + if isinstance(ch, bytes): + ch = self._decode(ch)[0] + else: + ch = ch[0] + self.w[r-1][c-1] = ch + + def put (self, ch): + '''This puts a characters at the current cursor position. + ''' + + if isinstance(ch, bytes): + ch = self._decode(ch) + + self.put_abs (self.cur_r, self.cur_c, ch) + + def insert_abs (self, r, c, ch): + '''This inserts a character at (r,c). Everything under + and to the right is shifted right one character. + The last character of the line is lost. + ''' + + if isinstance(ch, bytes): + ch = self._decode(ch) + + r = constrain (r, 1, self.rows) + c = constrain (c, 1, self.cols) + for ci in range (self.cols, c, -1): + self.put_abs (r,ci, self.get_abs(r,ci-1)) + self.put_abs (r,c,ch) + + def insert (self, ch): + + if isinstance(ch, bytes): + ch = self._decode(ch) + + self.insert_abs (self.cur_r, self.cur_c, ch) + + def get_abs (self, r, c): + + r = constrain (r, 1, self.rows) + c = constrain (c, 1, self.cols) + return self.w[r-1][c-1] + + def get (self): + + self.get_abs (self.cur_r, self.cur_c) + + def get_region (self, rs,cs, re,ce): + '''This returns a list of lines representing the region. + ''' + + rs = constrain (rs, 1, self.rows) + re = constrain (re, 1, self.rows) + cs = constrain (cs, 1, self.cols) + ce = constrain (ce, 1, self.cols) + if rs > re: + rs, re = re, rs + if cs > ce: + cs, ce = ce, cs + sc = [] + for r in range (rs, re+1): + line = u'' + for c in range (cs, ce + 1): + ch = self.get_abs (r,c) + line = line + ch + sc.append (line) + return sc + + def cursor_constrain (self): + '''This keeps the cursor within the screen area. + ''' + + self.cur_r = constrain (self.cur_r, 1, self.rows) + self.cur_c = constrain (self.cur_c, 1, self.cols) + + def cursor_home (self, r=1, c=1): # [{ROW};{COLUMN}H + + self.cur_r = r + self.cur_c = c + self.cursor_constrain () + + def cursor_back (self,count=1): # [{COUNT}D (not confused with down) + + self.cur_c = self.cur_c - count + self.cursor_constrain () + + def cursor_down (self,count=1): # [{COUNT}B (not confused with back) + + self.cur_r = self.cur_r + count + self.cursor_constrain () + + def cursor_forward (self,count=1): # [{COUNT}C + + self.cur_c = self.cur_c + count + self.cursor_constrain () + + def cursor_up (self,count=1): # [{COUNT}A + + self.cur_r = self.cur_r - count + self.cursor_constrain () + + def cursor_up_reverse (self): # M (called RI -- Reverse Index) + + old_r = self.cur_r + self.cursor_up() + if old_r == self.cur_r: + self.scroll_up() + + def cursor_force_position (self, r, c): # [{ROW};{COLUMN}f + '''Identical to Cursor Home.''' + + self.cursor_home (r, c) + + def cursor_save (self): # [s + '''Save current cursor position.''' + + self.cursor_save_attrs() + + def cursor_unsave (self): # [u + '''Restores cursor position after a Save Cursor.''' + + self.cursor_restore_attrs() + + def cursor_save_attrs (self): # 7 + '''Save current cursor position.''' + + self.cur_saved_r = self.cur_r + self.cur_saved_c = self.cur_c + + def cursor_restore_attrs (self): # 8 + '''Restores cursor position after a Save Cursor.''' + + self.cursor_home (self.cur_saved_r, self.cur_saved_c) + + def scroll_constrain (self): + '''This keeps the scroll region within the screen region.''' + + if self.scroll_row_start <= 0: + self.scroll_row_start = 1 + if self.scroll_row_end > self.rows: + self.scroll_row_end = self.rows + + def scroll_screen (self): # [r + '''Enable scrolling for entire display.''' + + self.scroll_row_start = 1 + self.scroll_row_end = self.rows + + def scroll_screen_rows (self, rs, re): # [{start};{end}r + '''Enable scrolling from row {start} to row {end}.''' + + self.scroll_row_start = rs + self.scroll_row_end = re + self.scroll_constrain() + + def scroll_down (self): # D + '''Scroll display down one line.''' + + # Screen is indexed from 1, but arrays are indexed from 0. + s = self.scroll_row_start - 1 + e = self.scroll_row_end - 1 + self.w[s+1:e+1] = copy.deepcopy(self.w[s:e]) + + def scroll_up (self): # M + '''Scroll display up one line.''' + + # Screen is indexed from 1, but arrays are indexed from 0. + s = self.scroll_row_start - 1 + e = self.scroll_row_end - 1 + self.w[s:e] = copy.deepcopy(self.w[s+1:e+1]) + + def erase_end_of_line (self): # [0K -or- [K + '''Erases from the current cursor position to the end of the current + line.''' + + self.fill_region (self.cur_r, self.cur_c, self.cur_r, self.cols) + + def erase_start_of_line (self): # [1K + '''Erases from the current cursor position to the start of the current + line.''' + + self.fill_region (self.cur_r, 1, self.cur_r, self.cur_c) + + def erase_line (self): # [2K + '''Erases the entire current line.''' + + self.fill_region (self.cur_r, 1, self.cur_r, self.cols) + + def erase_down (self): # [0J -or- [J + '''Erases the screen from the current line down to the bottom of the + screen.''' + + self.erase_end_of_line () + self.fill_region (self.cur_r + 1, 1, self.rows, self.cols) + + def erase_up (self): # [1J + '''Erases the screen from the current line up to the top of the + screen.''' + + self.erase_start_of_line () + self.fill_region (self.cur_r-1, 1, 1, self.cols) + + def erase_screen (self): # [2J + '''Erases the screen with the background color.''' + + self.fill () + + def set_tab (self): # H + '''Sets a tab at the current position.''' + + pass + + def clear_tab (self): # [g + '''Clears tab at the current position.''' + + pass + + def clear_all_tabs (self): # [3g + '''Clears all tabs.''' + + pass + +# Insert line Esc [ Pn L +# Delete line Esc [ Pn M +# Delete character Esc [ Pn P +# Scrolling region Esc [ Pn(top);Pn(bot) r + diff --git a/class/pexpect/spawnbase.py b/class/pexpect/spawnbase.py new file mode 100644 index 00000000..aacb63a4 --- /dev/null +++ b/class/pexpect/spawnbase.py @@ -0,0 +1,525 @@ +from io import StringIO, BytesIO +import codecs +import os +import sys +import re +import errno +from .exceptions import ExceptionPexpect, EOF, TIMEOUT +from .expect import Expecter, searcher_string, searcher_re + +PY3 = (sys.version_info[0] >= 3) +text_type = str if PY3 else unicode + +class _NullCoder(object): + """Pass bytes through unchanged.""" + @staticmethod + def encode(b, final=False): + return b + + @staticmethod + def decode(b, final=False): + return b + +class SpawnBase(object): + """A base class providing the backwards-compatible spawn API for Pexpect. + + This should not be instantiated directly: use :class:`pexpect.spawn` or + :class:`pexpect.fdpexpect.fdspawn`. + """ + encoding = None + pid = None + flag_eof = False + + def __init__(self, timeout=30, maxread=2000, searchwindowsize=None, + logfile=None, encoding=None, codec_errors='strict'): + self.stdin = sys.stdin + self.stdout = sys.stdout + self.stderr = sys.stderr + + self.searcher = None + self.ignorecase = False + self.before = None + self.after = None + self.match = None + self.match_index = None + self.terminated = True + self.exitstatus = None + self.signalstatus = None + # status returned by os.waitpid + self.status = None + # the child file descriptor is initially closed + self.child_fd = -1 + self.timeout = timeout + self.delimiter = EOF + self.logfile = logfile + # input from child (read_nonblocking) + self.logfile_read = None + # output to send (send, sendline) + self.logfile_send = None + # max bytes to read at one time into buffer + self.maxread = maxread + # Data before searchwindowsize point is preserved, but not searched. + self.searchwindowsize = searchwindowsize + # Delay used before sending data to child. Time in seconds. + # Set this to None to skip the time.sleep() call completely. + self.delaybeforesend = 0.05 + # Used by close() to give kernel time to update process status. + # Time in seconds. + self.delayafterclose = 0.1 + # Used by terminate() to give kernel time to update process status. + # Time in seconds. + self.delayafterterminate = 0.1 + # Delay in seconds to sleep after each call to read_nonblocking(). + # Set this to None to skip the time.sleep() call completely: that + # would restore the behavior from pexpect-2.0 (for performance + # reasons or because you don't want to release Python's global + # interpreter lock). + self.delayafterread = 0.0001 + self.softspace = False + self.name = '<' + repr(self) + '>' + self.closed = True + + # Unicode interface + self.encoding = encoding + self.codec_errors = codec_errors + if encoding is None: + # bytes mode (accepts some unicode for backwards compatibility) + self._encoder = self._decoder = _NullCoder() + self.string_type = bytes + self.buffer_type = BytesIO + self.crlf = b'\r\n' + if PY3: + self.allowed_string_types = (bytes, str) + self.linesep = os.linesep.encode('ascii') + def write_to_stdout(b): + try: + return sys.stdout.buffer.write(b) + except AttributeError: + # If stdout has been replaced, it may not have .buffer + return sys.stdout.write(b.decode('ascii', 'replace')) + self.write_to_stdout = write_to_stdout + else: + self.allowed_string_types = (basestring,) # analysis:ignore + self.linesep = os.linesep + self.write_to_stdout = sys.stdout.write + else: + # unicode mode + self._encoder = codecs.getincrementalencoder(encoding)(codec_errors) + self._decoder = codecs.getincrementaldecoder(encoding)(codec_errors) + self.string_type = text_type + self.buffer_type = StringIO + self.crlf = u'\r\n' + self.allowed_string_types = (text_type, ) + if PY3: + self.linesep = os.linesep + else: + self.linesep = os.linesep.decode('ascii') + # This can handle unicode in both Python 2 and 3 + self.write_to_stdout = sys.stdout.write + # storage for async transport + self.async_pw_transport = None + # This is the read buffer. See maxread. + self._buffer = self.buffer_type() + # The buffer may be trimmed for efficiency reasons. This is the + # untrimmed buffer, used to create the before attribute. + self._before = self.buffer_type() + + def _log(self, s, direction): + if self.logfile is not None: + self.logfile.write(s) + self.logfile.flush() + second_log = self.logfile_send if (direction=='send') else self.logfile_read + if second_log is not None: + second_log.write(s) + second_log.flush() + + # For backwards compatibility, in bytes mode (when encoding is None) + # unicode is accepted for send and expect. Unicode mode is strictly unicode + # only. + def _coerce_expect_string(self, s): + if self.encoding is None and not isinstance(s, bytes): + return s.encode('ascii') + return s + + def _coerce_send_string(self, s): + if self.encoding is None and not isinstance(s, bytes): + return s.encode('utf-8') + return s + + def _get_buffer(self): + return self._buffer.getvalue() + + def _set_buffer(self, value): + self._buffer = self.buffer_type() + self._buffer.write(value) + + # This property is provided for backwards compatibility (self.buffer used + # to be a string/bytes object) + buffer = property(_get_buffer, _set_buffer) + + def read_nonblocking(self, size=1, timeout=None): + """This reads data from the file descriptor. + + This is a simple implementation suitable for a regular file. Subclasses using ptys or pipes should override it. + + The timeout parameter is ignored. + """ + + try: + s = os.read(self.child_fd, size) + except OSError as err: + if err.args[0] == errno.EIO: + # Linux-style EOF + self.flag_eof = True + raise EOF('End Of File (EOF). Exception style platform.') + raise + if s == b'': + # BSD-style EOF + self.flag_eof = True + raise EOF('End Of File (EOF). Empty string style platform.') + + s = self._decoder.decode(s, final=False) + self._log(s, 'read') + return s + + def _pattern_type_err(self, pattern): + raise TypeError('got {badtype} ({badobj!r}) as pattern, must be one' + ' of: {goodtypes}, pexpect.EOF, pexpect.TIMEOUT'\ + .format(badtype=type(pattern), + badobj=pattern, + goodtypes=', '.join([str(ast)\ + for ast in self.allowed_string_types]) + ) + ) + + def compile_pattern_list(self, patterns): + '''This compiles a pattern-string or a list of pattern-strings. + Patterns must be a StringType, EOF, TIMEOUT, SRE_Pattern, or a list of + those. Patterns may also be None which results in an empty list (you + might do this if waiting for an EOF or TIMEOUT condition without + expecting any pattern). + + This is used by expect() when calling expect_list(). Thus expect() is + nothing more than:: + + cpl = self.compile_pattern_list(pl) + return self.expect_list(cpl, timeout) + + If you are using expect() within a loop it may be more + efficient to compile the patterns first and then call expect_list(). + This avoid calls in a loop to compile_pattern_list():: + + cpl = self.compile_pattern_list(my_pattern) + while some_condition: + ... + i = self.expect_list(cpl, timeout) + ... + ''' + + if patterns is None: + return [] + if not isinstance(patterns, list): + patterns = [patterns] + + # Allow dot to match \n + compile_flags = re.DOTALL + if self.ignorecase: + compile_flags = compile_flags | re.IGNORECASE + compiled_pattern_list = [] + for idx, p in enumerate(patterns): + if isinstance(p, self.allowed_string_types): + p = self._coerce_expect_string(p) + compiled_pattern_list.append(re.compile(p, compile_flags)) + elif p is EOF: + compiled_pattern_list.append(EOF) + elif p is TIMEOUT: + compiled_pattern_list.append(TIMEOUT) + elif isinstance(p, type(re.compile(''))): + compiled_pattern_list.append(p) + else: + self._pattern_type_err(p) + return compiled_pattern_list + + def expect(self, pattern, timeout=-1, searchwindowsize=-1, async_=False, **kw): + '''This seeks through the stream until a pattern is matched. The + pattern is overloaded and may take several types. The pattern can be a + StringType, EOF, a compiled re, or a list of any of those types. + Strings will be compiled to re types. This returns the index into the + pattern list. If the pattern was not a list this returns index 0 on a + successful match. This may raise exceptions for EOF or TIMEOUT. To + avoid the EOF or TIMEOUT exceptions add EOF or TIMEOUT to the pattern + list. That will cause expect to match an EOF or TIMEOUT condition + instead of raising an exception. + + If you pass a list of patterns and more than one matches, the first + match in the stream is chosen. If more than one pattern matches at that + point, the leftmost in the pattern list is chosen. For example:: + + # the input is 'foobar' + index = p.expect(['bar', 'foo', 'foobar']) + # returns 1('foo') even though 'foobar' is a "better" match + + Please note, however, that buffering can affect this behavior, since + input arrives in unpredictable chunks. For example:: + + # the input is 'foobar' + index = p.expect(['foobar', 'foo']) + # returns 0('foobar') if all input is available at once, + # but returns 1('foo') if parts of the final 'bar' arrive late + + When a match is found for the given pattern, the class instance + attribute *match* becomes an re.MatchObject result. Should an EOF + or TIMEOUT pattern match, then the match attribute will be an instance + of that exception class. The pairing before and after class + instance attributes are views of the data preceding and following + the matching pattern. On general exception, class attribute + *before* is all data received up to the exception, while *match* and + *after* attributes are value None. + + When the keyword argument timeout is -1 (default), then TIMEOUT will + raise after the default value specified by the class timeout + attribute. When None, TIMEOUT will not be raised and may block + indefinitely until match. + + When the keyword argument searchwindowsize is -1 (default), then the + value specified by the class maxread attribute is used. + + A list entry may be EOF or TIMEOUT instead of a string. This will + catch these exceptions and return the index of the list entry instead + of raising the exception. The attribute 'after' will be set to the + exception type. The attribute 'match' will be None. This allows you to + write code like this:: + + index = p.expect(['good', 'bad', pexpect.EOF, pexpect.TIMEOUT]) + if index == 0: + do_something() + elif index == 1: + do_something_else() + elif index == 2: + do_some_other_thing() + elif index == 3: + do_something_completely_different() + + instead of code like this:: + + try: + index = p.expect(['good', 'bad']) + if index == 0: + do_something() + elif index == 1: + do_something_else() + except EOF: + do_some_other_thing() + except TIMEOUT: + do_something_completely_different() + + These two forms are equivalent. It all depends on what you want. You + can also just expect the EOF if you are waiting for all output of a + child to finish. For example:: + + p = pexpect.spawn('/bin/ls') + p.expect(pexpect.EOF) + print p.before + + If you are trying to optimize for speed then see expect_list(). + + On Python 3.4, or Python 3.3 with asyncio installed, passing + ``async_=True`` will make this return an :mod:`asyncio` coroutine, + which you can yield from to get the same result that this method would + normally give directly. So, inside a coroutine, you can replace this code:: + + index = p.expect(patterns) + + With this non-blocking form:: + + index = yield from p.expect(patterns, async_=True) + ''' + if 'async' in kw: + async_ = kw.pop('async') + if kw: + raise TypeError("Unknown keyword arguments: {}".format(kw)) + + compiled_pattern_list = self.compile_pattern_list(pattern) + return self.expect_list(compiled_pattern_list, + timeout, searchwindowsize, async_) + + def expect_list(self, pattern_list, timeout=-1, searchwindowsize=-1, + async_=False, **kw): + '''This takes a list of compiled regular expressions and returns the + index into the pattern_list that matched the child output. The list may + also contain EOF or TIMEOUT(which are not compiled regular + expressions). This method is similar to the expect() method except that + expect_list() does not recompile the pattern list on every call. This + may help if you are trying to optimize for speed, otherwise just use + the expect() method. This is called by expect(). + + + Like :meth:`expect`, passing ``async_=True`` will make this return an + asyncio coroutine. + ''' + if timeout == -1: + timeout = self.timeout + if 'async' in kw: + async_ = kw.pop('async') + if kw: + raise TypeError("Unknown keyword arguments: {}".format(kw)) + + exp = Expecter(self, searcher_re(pattern_list), searchwindowsize) + if async_: + from ._async import expect_async + return expect_async(exp, timeout) + else: + return exp.expect_loop(timeout) + + def expect_exact(self, pattern_list, timeout=-1, searchwindowsize=-1, + async_=False, **kw): + + '''This is similar to expect(), but uses plain string matching instead + of compiled regular expressions in 'pattern_list'. The 'pattern_list' + may be a string; a list or other sequence of strings; or TIMEOUT and + EOF. + + This call might be faster than expect() for two reasons: string + searching is faster than RE matching and it is possible to limit the + search to just the end of the input buffer. + + This method is also useful when you don't want to have to worry about + escaping regular expression characters that you want to match. + + Like :meth:`expect`, passing ``async_=True`` will make this return an + asyncio coroutine. + ''' + if timeout == -1: + timeout = self.timeout + if 'async' in kw: + async_ = kw.pop('async') + if kw: + raise TypeError("Unknown keyword arguments: {}".format(kw)) + + if (isinstance(pattern_list, self.allowed_string_types) or + pattern_list in (TIMEOUT, EOF)): + pattern_list = [pattern_list] + + def prepare_pattern(pattern): + if pattern in (TIMEOUT, EOF): + return pattern + if isinstance(pattern, self.allowed_string_types): + return self._coerce_expect_string(pattern) + self._pattern_type_err(pattern) + + try: + pattern_list = iter(pattern_list) + except TypeError: + self._pattern_type_err(pattern_list) + pattern_list = [prepare_pattern(p) for p in pattern_list] + + exp = Expecter(self, searcher_string(pattern_list), searchwindowsize) + if async_: + from ._async import expect_async + return expect_async(exp, timeout) + else: + return exp.expect_loop(timeout) + + def expect_loop(self, searcher, timeout=-1, searchwindowsize=-1): + '''This is the common loop used inside expect. The 'searcher' should be + an instance of searcher_re or searcher_string, which describes how and + what to search for in the input. + + See expect() for other arguments, return value and exceptions. ''' + + exp = Expecter(self, searcher, searchwindowsize) + return exp.expect_loop(timeout) + + def read(self, size=-1): + '''This reads at most "size" bytes from the file (less if the read hits + EOF before obtaining size bytes). If the size argument is negative or + omitted, read all data until EOF is reached. The bytes are returned as + a string object. An empty string is returned when EOF is encountered + immediately. ''' + + if size == 0: + return self.string_type() + if size < 0: + # delimiter default is EOF + self.expect(self.delimiter) + return self.before + + # I could have done this more directly by not using expect(), but + # I deliberately decided to couple read() to expect() so that + # I would catch any bugs early and ensure consistent behavior. + # It's a little less efficient, but there is less for me to + # worry about if I have to later modify read() or expect(). + # Note, it's OK if size==-1 in the regex. That just means it + # will never match anything in which case we stop only on EOF. + cre = re.compile(self._coerce_expect_string('.{%d}' % size), re.DOTALL) + # delimiter default is EOF + index = self.expect([cre, self.delimiter]) + if index == 0: + ### FIXME self.before should be ''. Should I assert this? + return self.after + return self.before + + def readline(self, size=-1): + '''This reads and returns one entire line. The newline at the end of + line is returned as part of the string, unless the file ends without a + newline. An empty string is returned if EOF is encountered immediately. + This looks for a newline as a CR/LF pair (\\r\\n) even on UNIX because + this is what the pseudotty device returns. So contrary to what you may + expect you will receive newlines as \\r\\n. + + If the size argument is 0 then an empty string is returned. In all + other cases the size argument is ignored, which is not standard + behavior for a file-like object. ''' + + if size == 0: + return self.string_type() + # delimiter default is EOF + index = self.expect([self.crlf, self.delimiter]) + if index == 0: + return self.before + self.crlf + else: + return self.before + + def __iter__(self): + '''This is to support iterators over a file-like object. + ''' + return iter(self.readline, self.string_type()) + + def readlines(self, sizehint=-1): + '''This reads until EOF using readline() and returns a list containing + the lines thus read. The optional 'sizehint' argument is ignored. + Remember, because this reads until EOF that means the child + process should have closed its stdout. If you run this method on + a child that is still running with its stdout open then this + method will block until it timesout.''' + + lines = [] + while True: + line = self.readline() + if not line: + break + lines.append(line) + return lines + + def fileno(self): + '''Expose file descriptor for a file-like interface + ''' + return self.child_fd + + def flush(self): + '''This does nothing. It is here to support the interface for a + File-like object. ''' + pass + + def isatty(self): + """Overridden in subclass using tty""" + return False + + # For 'with spawn(...) as child:' + def __enter__(self): + return self + + def __exit__(self, etype, evalue, tb): + # We rely on subclasses to implement close(). If they don't, it's not + # clear what a context manager should do. + self.close() diff --git a/class/pexpect/utils.py b/class/pexpect/utils.py new file mode 100644 index 00000000..f7745196 --- /dev/null +++ b/class/pexpect/utils.py @@ -0,0 +1,187 @@ +import os +import sys +import stat +import select +import time +import errno + +try: + InterruptedError +except NameError: + # Alias Python2 exception to Python3 + InterruptedError = select.error + +if sys.version_info[0] >= 3: + string_types = (str,) +else: + string_types = (unicode, str) + + +def is_executable_file(path): + """Checks that path is an executable regular file, or a symlink towards one. + + This is roughly ``os.path isfile(path) and os.access(path, os.X_OK)``. + """ + # follow symlinks, + fpath = os.path.realpath(path) + + if not os.path.isfile(fpath): + # non-files (directories, fifo, etc.) + return False + + mode = os.stat(fpath).st_mode + + if (sys.platform.startswith('sunos') + and os.getuid() == 0): + # When root on Solaris, os.X_OK is True for *all* files, irregardless + # of their executability -- instead, any permission bit of any user, + # group, or other is fine enough. + # + # (This may be true for other "Unix98" OS's such as HP-UX and AIX) + return bool(mode & (stat.S_IXUSR | + stat.S_IXGRP | + stat.S_IXOTH)) + + return os.access(fpath, os.X_OK) + + +def which(filename, env=None): + '''This takes a given filename; tries to find it in the environment path; + then checks if it is executable. This returns the full path to the filename + if found and executable. Otherwise this returns None.''' + + # Special case where filename contains an explicit path. + if os.path.dirname(filename) != '' and is_executable_file(filename): + return filename + if env is None: + env = os.environ + p = env.get('PATH') + if not p: + p = os.defpath + pathlist = p.split(os.pathsep) + for path in pathlist: + ff = os.path.join(path, filename) + if is_executable_file(ff): + return ff + return None + + +def split_command_line(command_line): + + '''This splits a command line into a list of arguments. It splits arguments + on spaces, but handles embedded quotes, doublequotes, and escaped + characters. It's impossible to do this with a regular expression, so I + wrote a little state machine to parse the command line. ''' + + arg_list = [] + arg = '' + + # Constants to name the states we can be in. + state_basic = 0 + state_esc = 1 + state_singlequote = 2 + state_doublequote = 3 + # The state when consuming whitespace between commands. + state_whitespace = 4 + state = state_basic + + for c in command_line: + if state == state_basic or state == state_whitespace: + if c == '\\': + # Escape the next character + state = state_esc + elif c == r"'": + # Handle single quote + state = state_singlequote + elif c == r'"': + # Handle double quote + state = state_doublequote + elif c.isspace(): + # Add arg to arg_list if we aren't in the middle of whitespace. + if state == state_whitespace: + # Do nothing. + None + else: + arg_list.append(arg) + arg = '' + state = state_whitespace + else: + arg = arg + c + state = state_basic + elif state == state_esc: + arg = arg + c + state = state_basic + elif state == state_singlequote: + if c == r"'": + state = state_basic + else: + arg = arg + c + elif state == state_doublequote: + if c == r'"': + state = state_basic + else: + arg = arg + c + + if arg != '': + arg_list.append(arg) + return arg_list + + +def select_ignore_interrupts(iwtd, owtd, ewtd, timeout=None): + + '''This is a wrapper around select.select() that ignores signals. If + select.select raises a select.error exception and errno is an EINTR + error then it is ignored. Mainly this is used to ignore sigwinch + (terminal resize). ''' + + # if select() is interrupted by a signal (errno==EINTR) then + # we loop back and enter the select() again. + if timeout is not None: + end_time = time.time() + timeout + while True: + try: + return select.select(iwtd, owtd, ewtd, timeout) + except InterruptedError: + err = sys.exc_info()[1] + if err.args[0] == errno.EINTR: + # if we loop back we have to subtract the + # amount of time we already waited. + if timeout is not None: + timeout = end_time - time.time() + if timeout < 0: + return([], [], []) + else: + # something else caused the select.error, so + # this actually is an exception. + raise + + +def poll_ignore_interrupts(fds, timeout=None): + '''Simple wrapper around poll to register file descriptors and + ignore signals.''' + + if timeout is not None: + end_time = time.time() + timeout + + poller = select.poll() + for fd in fds: + poller.register(fd, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR) + + while True: + try: + timeout_ms = None if timeout is None else timeout * 1000 + results = poller.poll(timeout_ms) + return [afd for afd, _ in results] + except InterruptedError: + err = sys.exc_info()[1] + if err.args[0] == errno.EINTR: + # if we loop back we have to subtract the + # amount of time we already waited. + if timeout is not None: + timeout = end_time - time.time() + if timeout < 0: + return [] + else: + # something else caused the select.error, so + # this actually is an exception. + raise diff --git a/class/projectModel/__init__.py b/class/projectModel/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/class/projectModel/__init__.py @@ -0,0 +1 @@ + diff --git a/class/projectModel/base.py b/class/projectModel/base.py new file mode 100644 index 00000000..59c8de8b --- /dev/null +++ b/class/projectModel/base.py @@ -0,0 +1,2 @@ +class projectBase: + pass \ No newline at end of file diff --git a/class/projectModel/nodejsModel.py b/class/projectModel/nodejsModel.py new file mode 100644 index 00000000..b0803962 --- /dev/null +++ b/class/projectModel/nodejsModel.py @@ -0,0 +1,1712 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# 宝塔Linux面板 +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# node.js模型 +#------------------------------ +import os,sys,re,json,shutil,psutil,time +from projectModel.base import projectBase +import public +try: + from BTPanel import cache +except: + pass + +class main(projectBase): + _panel_path = public.get_panel_path() + _nodejs_plugin_path = public.get_plugin_path('nodejs') + _nodejs_path = '{}/nodejs'.format(public.get_setup_path()) + _log_name = 'Project management' + _npm_exec_log = '{}/logs/npm-exec.log'.format(_panel_path) + _node_pid_path = '{}/vhost/pids'.format(_nodejs_path) + _node_logs_path = '{}/vhost/logs'.format(_nodejs_path) + _node_run_scripts = '{}/vhost/scripts'.format(_nodejs_path) + _pids = None + _vhost_path = '{}/vhost'.format(_panel_path) + _www_home = '/home/www' + + + + def __init__(self): + if not os.path.exists(self._node_run_scripts): + os.makedirs(self._node_run_scripts,493) + + if not os.path.exists(self._node_pid_path): + os.makedirs(self._node_pid_path,493) + + if not os.path.exists(self._node_logs_path): + os.makedirs(self._node_logs_path,493) + + if not os.path.exists(self._www_home): + os.makedirs(self._www_home,493) + public.set_own(self._www_home,'www') + + + def get_exec_logs(self,get): + ''' + @name 获取执行日志 + @author hwliang<2021-08-09> + @param get + @return string + ''' + if not os.path.exists(self._npm_exec_log): return public.returnMsg(False,'NODE_NOT_EXISTS') + return public.GetNumLines(self._npm_exec_log,20) + + + def get_project_list(self,get): + ''' + @name 获取项目列表 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + + if not 'p' in get: get.p = 1 + if not 'limit' in get: get.limit = 20 + if not 'callback' in get: get.callback = '' + if not 'order' in get: get.order = 'id desc' + + if 'search' in get: + get.project_name = get.search.strip() + search = "%{}%".format(get.project_name) + count = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)',('Node',search,search)).count() + data = public.get_page(count,int(get.p),int(get.limit),get.callback) + data['data'] = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)',('Node',search,search)).limit(data['shift'] + ',' + data['row']).order(get.order).select() + else: + count = public.M('sites').where('project_type=?','Node').count() + data = public.get_page(count,int(get.p),int(get.limit),get.callback) + data['data'] = public.M('sites').where('project_type=?','Node').limit(data['shift'] + ',' + data['row']).order(get.order).select() + + for i in range(len(data['data'])): + data['data'][i] = self.get_project_stat(data['data'][i]) + return data + + + def get_ssl_end_date(self,project_name): + ''' + @name 获取SSL信息 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + import data + return data.data().get_site_ssl_info('node_{}'.format(project_name)) + + + + def is_install_nodejs(self,get): + ''' + @name 是否安装nodejs版本管理器 + @author hwliang<2021-08-09> + @param get 请求数据 + @return bool + ''' + return os.path.exists(self._nodejs_plugin_path) + + + def get_nodejs_version(self,get): + ''' + @name 获取已安装的nodejs版本 + @author hwliang<2021-08-09> + @param get 请求数据 + @return list + ''' + nodejs_list = [] + if not os.path.exists(self._nodejs_path): return nodejs_list + for v in os.listdir(self._nodejs_path): + if v[0] != 'v' or v.find('.') == -1: continue + nodejs_list.append(v) + return nodejs_list + + + + def get_run_list(self,get): + ''' + @name 获取node项目启动列表 + @author hwliang<2021-08-10> + @param get{ + project_cwd: string<项目目录> + } + ''' + project_cwd = get.project_cwd.strip() + if not os.path.exists(project_cwd): return public.return_error('The project directory does not exist!') + package_file = '{}/package.json'.format(project_cwd) + if not os.path.exists(package_file): return {} #public.return_error('没有在项目目录中找到package.json配置文件!') + package_info = json.loads(public.readFile(package_file)) + if not 'scripts' in package_info: return {}# public.return_error('没有在项目配置文件package.json中找到scripts配置项!') + if not package_info['scripts']: return {}# public.return_error('没有找到可用的启动选项!') + return package_info['scripts'] + + + def get_npm_bin(self,nodejs_version): + ''' + @name 获取指定node版本的npm路径 + @author hwliang<2021-08-10> + @param nodejs_version nodejs版本 + @return string + ''' + npm_path = '{}/{}/bin/npm'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(npm_path): return False + return npm_path + + def get_yarn_bin(self,nodejs_version): + ''' + @name 获取指定node版本的yarn路径 + @author hwliang<2021-08-28> + @param nodejs_version nodejs版本 + @return string + ''' + yarn_path = '{}/{}/bin/yarn'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(yarn_path): return False + return yarn_path + + + def get_node_bin(self,nodejs_version): + ''' + @name 获取指定node版本的node路径 + @author hwliang<2021-08-10> + @param nodejs_version nodejs版本 + @return string + ''' + node_path = '{}/{}/bin/node'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(node_path): return False + return node_path + + + def get_last_env(self,nodejs_version,project_cwd = None): + ''' + @name 获取前置环境变量 + @author hwliang<2021-08-25> + @param nodejs_version Node版本 + @return string + ''' + nodejs_bin_path = '{}/{}/bin'.format(self._nodejs_path,nodejs_version) + if project_cwd: + _bin = '{}/node_modules/.bin'.format(project_cwd) + if os.path.exists(_bin): + nodejs_bin_path = _bin + ':' + nodejs_bin_path + + last_env = '''PATH={nodejs_bin_path}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH +'''.format(nodejs_bin_path = nodejs_bin_path) + return last_env + + + def install_packages(self,get): + ''' + @name 安装指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + if not os.path.exists(project_find['path']): return public.return_error('The project directory does not exist!') + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): return public.return_error('The package.json configuration file was not found in the project directory!') + nodejs_version = project_find['project_config']['nodejs_version'] + + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + node_modules_path = '{}/node_modules'.format(project_find['path']) + + # 已经安装过的依赖包的情况下,可能存在不同node版本导致的问题,可能需要重新构建依赖包 + rebuild = False + if os.path.exists(package_lock_file) and os.path.exists(node_modules_path): + rebuild = True + + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + if not npm_bin and not yarn_bin: + return public.return_error('The specified nodejs version does not exist!') + public.writeFile(self._npm_exec_log,"Installing dependencies...\n") + public.writeFile(self._npm_exec_log,"Downloading dependency package, please wait...\n") + if yarn_bin: + if os.path.exists(package_lock_file): + os.remove(package_lock_file) + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install 2>&1 >> {}".format(project_find['path'],yarn_bin,self._npm_exec_log)) + else: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install 2>&1 >> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + public.writeFile(self._npm_exec_log,"|-Successify --- Command executed! ---",'a+') + public.WriteLog(self._log_name, 'Node project: {}, the installation of the dependency package is complete!'.format(project_find['name'])) + if rebuild: # 重新构建已安装模块? + self.rebuild_project(get.project_name) + return public.return_data(True,'The dependency package is installed successfully!') + + + + def update_packages(self,get): + ''' + @name 更新指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + if not os.path.exists(project_find['path']): return public.return_error('The project directory does not exist!') + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): return public.return_error('The package.json configuration file was not found in the project directory!') + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + if not os.path.exists(package_lock_file): return public.return_error('Please install the dependency package first!') + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + if not npm_bin: + return public.return_error('The specified nodejs version does not exist!') + + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} update &> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + public.WriteLog(self._log_name, 'Project [{}] update all dependent packages'.format(get.project_name)) + return public.return_data(True,'Dependent package updated successfully!') + + + def reinstall_packages(self,get): + ''' + @name 重新安装指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + if not os.path.exists(project_find['path']): return public.return_error('The project directory does not exist!') + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): return public.return_error('The package.json configuration file was not found in the project directory!') + + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + if os.path.exists(package_lock_file): os.remove(package_lock_file) + package_path = '{}/node_modules' + if os.path.exists(package_path): shutil.rmtree(package_path) + + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + if not npm_bin: + return public.return_error('The specified nodejs version does not exist!') + public.WriteLog(self._log_name,'Node project: {}, all dependent packages have been reinstalled') + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install &> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + return public.return_data(True,'Dependent package reinstalled successfully!') + + def get_project_modules(self,get): + ''' + @name 获取指定项目的依赖包列表 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> 可选 + } + return list + ''' + if not 'project_cwd' in get: + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + project_cwd = project_find['path'] + else: + project_cwd = get.project_cwd + mod_path = os.path.join(project_cwd,'node_modules') + modules = [] + if not os.path.exists(mod_path): return modules + for mod_name in os.listdir(mod_path): + try: + mod_pack_file = os.path.join(mod_path,mod_name,'package.json') + if not os.path.exists(mod_pack_file): continue + mod_pack_info = json.loads(public.readFile(mod_pack_file)) + pack_info = { + "name": mod_name, + "version": mod_pack_info['version'], + "description":mod_pack_info['description'], + "license": mod_pack_info['license'] if 'license' in mod_pack_info else 'NULL', + "homepage": mod_pack_info['homepage'] + } + modules.append(pack_info) + except: + continue + return modules + + def install_module(self,get): + ''' + @name 安装指定模块 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + project_cwd = project_find['path'] + + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if os.path.exists(filename): return public.return_error('The specified module has been installed!') + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + + if not npm_bin and not yarn_bin: + return public.return_error('The specified nodejs version does not exist!') + if yarn_bin: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} add {} &> {}".format(project_find['path'],yarn_bin,mod_name,self._npm_exec_log)) + else: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install {} &> {}".format(project_find['path'],npm_bin,mod_name,self._npm_exec_log)) + if not os.path.exists(filename): return public.return_error('Failed to install the specified module!') + public.WriteLog(self._log_name,'Node project {}, {} module installation is complete!'.format(get.project_name,mod_name)) + return public.return_data(True,'Successful installation!') + + def uninstall_module(self,get): + ''' + @name 卸载指定模块 + @author hwliang<2021-04-08> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + project_cwd = project_find['path'] + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if not os.path.exists(filename): return public.return_error('The specified module is not installed!') + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + if not npm_bin and not yarn_bin: + return public.return_error('The specified nodejs version does not exist!') + if yarn_bin: + result = public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} remove {}".format(project_find['path'],yarn_bin,mod_name)) + else: + result = public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} uninstall {}".format(project_find['path'],npm_bin,mod_name)) + if os.path.exists(filename): + result = "\n".join(result) + if result.find('looking for funding') != -1: + return public.return_error("This module is dependent on other installed modules and cannot be uninstalled!") + return public.return_error("Unable to uninstall this module!") + + public.WriteLog(self._log_name,'Node project {}, {} module uninstallation completed!'.format(get.project_name,mod_name)) + return public.return_data(True,'Module unloaded successfully!') + + + def upgrade_module(self,get): + ''' + @name 更新指定模块 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('The specified item does not exist!') + project_cwd = project_find['path'] + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if not os.path.exists(filename): return public.return_error('The specified module is not installed!') + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + + if not npm_bin: + return public.return_error('The specified nodejs version does not exist!') + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} update {} &> {}".format(project_find['path'],npm_bin,mod_name,self._npm_exec_log)) + public.WriteLog(self._log_name,'Node project {}, {} module update completed!'.format(get.project_name,mod_name)) + return public.return_data(True,'Module updated successfully!') + + + def create_project(self,get): + ''' + @name 创建新的项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> + project_script: string<项目脚本> + project_ps: string<项目备注信息> + bind_extranet: int<是否绑定外网> 1:是 0:否 + domains: list<域名列表> ["domain1:80","domain2:80"] // 在bind_extranet=1时,需要填写 + is_power_on: int<是否开机启动> 1:是 0:否 + run_user: string<运行用户> + max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 + nodejs_version: string + } + @return dict + ''' + if not isinstance(get,public.dict_obj): return public.return_error('The parameter type is wrong, need dict obj object') + if not self.is_install_nodejs(get): + return public.return_error('Please install nodejs version manager first') + + project_name = get.project_name.strip() + if not re.match("^\w+$",project_name): + return public.return_error('The project name format is incorrect and supports letters, numbers, underscores, and expressions: ^[0-9A-Za-z_]$') + + if public.M('sites').where('name=?',(get.project_name,)).count(): + return public.return_error('The specified project name already exists: {}'.format(get.project_name)) + get.project_cwd = get.project_cwd.strip() + if not os.path.exists(get.project_cwd): + return public.return_error('The project directory does not exist: {}'.format(get.project_cwd)) + + # 端口占用检测 + if self.check_port_is_used(get.get('port/port')): + return public.return_error('This port is already occupied, please modify your project port, port: {}'.format(get.port)) + + domains = [] + if get.bind_extranet == 1: + domains = get.domains + if not public.is_apache_nginx(): return public.return_error('Please install Nginx or Apache first') + for domain in domains: + domain_arr = domain.split(':') + if public.M('domain').where('name=?',domain_arr[0]).count(): + return public.return_error('Domain name already exists: {}'.format(domain)) + pdata = { + 'name': get.project_name, + 'path': get.project_cwd, + 'ps': get.project_ps, + 'status':1, + 'type_id':0, + 'project_type': 'Node', + 'project_config': json.dumps( + { + 'project_name': get.project_name, + 'project_cwd': get.project_cwd, + 'project_script': get.project_script, + 'bind_extranet': get.bind_extranet, + 'domains': [], + 'is_power_on': get.is_power_on, + 'run_user': get.run_user, + 'max_memory_limit': get.max_memory_limit, + 'nodejs_version': get.nodejs_version, + 'port': int(get.port) + } + ), + 'addtime': public.getDate() + } + + project_id = public.M('sites').insert(pdata) + if get.bind_extranet == 1: + format_domains = [] + for domain in domains: + if domain.find(':') == -1: domain += ':80' + format_domains.append(domain) + get.domains = format_domains + self.project_add_domain(get) + self.set_config(get.project_name) + public.WriteLog(self._log_name,'Add Node.js project {}'.format(get.project_name)) + self.install_packages(get) + self.start_project(get) + return public.return_data(True,'Added project successfully',project_id) + + def modify_project(self,get): + ''' + @name 修改指定项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> + project_script: string<项目脚本> + project_ps: string<项目备注信息> + is_power_on: int<是否开机启动> 1:是 0:否 + run_user: string<运行用户> + max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 + nodejs_version: string + } + @return dict + ''' + if not isinstance(get,public.dict_obj): return public.return_error('The parameter type is wrong, need dict obj') + if not self.is_install_nodejs(get): + return public.return_error('Please install nodejs version manager before installing at least one nodejs') + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('Item does not exist: {}'.format(get.project_name)) + + if not os.path.exists(get.project_cwd): + return public.return_error('The project directory does not exist: {}'.format(get.project_cwd)) + rebuild = False + if hasattr(get,'port'): + if int(project_find['project_config']['port']) != int(get.port): + if self.check_port_is_used(get.get('port/port'),True): + return public.return_error('The port is already occupied, please modify your port, port: {}'.format(get.port)) + project_find['project_config']['port'] = int(get.port) + if hasattr(get,'project_cwd'): project_find['project_config']['project_cwd'] = get.project_cwd + if hasattr(get,'project_script'): + if not get.project_script.strip(): + return public.return_error('Start command cannot be empty') + project_find['project_config']['project_script'] = get.project_script.strip() + if hasattr(get,'is_power_on'): project_find['project_config']['is_power_on'] = get.is_power_on + if hasattr(get,'run_user'): project_find['project_config']['run_user'] = get.run_user + if hasattr(get,'max_memory_limit'): project_find['project_config']['max_memory_limit'] = get.max_memory_limit + if hasattr(get,'nodejs_version'): + if project_find['project_config']['nodejs_version'] != get.nodejs_version: + rebuild = True + project_find['project_config']['nodejs_version'] = get.nodejs_version + pdata = { + 'path': get.project_cwd, + 'ps': get.project_ps, + 'project_config': json.dumps(project_find['project_config']) + } + + public.M('sites').where('name=?',(get.project_name,)).update(pdata) + self.set_config(get.project_name) + public.WriteLog(self._log_name,'Modify Node.js project {}'.format(get.project_name)) + if rebuild: + self.rebuild_project(get.project_name) + return public.return_data(True,'Modify the project successfully') + + def rebuild_project(self,project_name): + ''' + @name 重新构建指定项目 + @author hwliang<2021-08-26> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} rebuild 2>&1 >> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + return True + + + def remove_project(self,get): + ''' + @name 删除指定项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('The specified item does not exist: {}'.format(get.project_name)) + + self.stop_project(get) + self.clear_config(get.project_name) + public.M('domain').where('pid=?',(project_find['id'],)).delete() + public.M('sites').where('name=?',(get.project_name,)).delete() + + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if os.path.exists(pid_file): os.remove(pid_file) + script_file = '{}/{}.sh'.format(self._node_run_scripts,get.project_name) + if os.path.exists(script_file): os.remove(script_file) + log_file = '{}/{}.log'.format(self._node_logs_path,get.project_name) + if os.path.exists(log_file): os.remove(log_file) + public.WriteLog(self._log_name,'Delete Node.js project {}'.format(get.project_name)) + return public.return_data(True,'Successfully deleted item') + + + def project_get_domain(self,get): + ''' + @name 获取指定项目的域名列表 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_id = public.M('sites').where('name=?',(get.project_name,)).getField('id') + domains = public.M('domain').where('pid=?',(project_id,)).order('id desc').select() + project_find = self.get_project_find(get.project_name) + if len(domains) != len(project_find['project_config']['domains']): + public.M('domain').where('pid=?',(project_id,)).delete() + if not project_find: return [] + for d in project_find['project_config']['domains']: + domain = {} + arr = d.split(':') + if len(arr) < 2: arr.append(80) + domain['name'] = arr[0] + domain['port'] = int(arr[1]) + domain['pid'] = project_id + domain['addtime'] = public.getDate() + public.M('domain').insert(domain) + if project_find['project_config']['domains']: + domains = public.M('domain').where('pid=?',(project_id,)).select() + return domains + + + def project_add_domain(self,get): + ''' + @name 为指定项目添加域名 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + domains: list<域名列表> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('The specified item does not exist') + project_id = project_find['id'] + + domains = get.domains + success_list = [] + error_list = [] + for domain in domains: + domain = domain.strip() + domain_arr = domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + domain += ':80' + if not public.M('domain').where('name=?',(domain_arr[0],)).count(): + public.M('domain').add('name,pid,port,addtime',(domain_arr[0],project_id,domain_arr[1],public.getDate())) + if not domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].append(domain) + public.WriteLog(self._log_name,'Successfully added the domain [{}] to the project [{}]'.format(domain,get.project_name)) + success_list.append(domain) + else: + public.WriteLog(self._log_name,'Domain [{}] already exists'.format(domain)) + error_list.append(domain) + + if success_list: + public.M('sites').where('id=?',(project_id,)).save('project_config',json.dumps(project_find['project_config'])) + self.set_config(get.project_name) + + return public.return_data(True,"[{}] domain names added successfully, [{}] failed!".format(len(success_list),len(error_list)),error_msg=error_list) + + + def project_remove_domain(self,get): + ''' + @name 为指定项目删除域名 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + domain: string<域名> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('The specified item does not exist') + last_domain = get.domain + domain_arr = get.domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + + project_id = public.M('sites').where('name=?',(get.project_name,)).getField('id') + if project_find['project_config']['bind_extranet']: + if len(project_find['project_config']['domains']) == 1: return public.return_error('At least one domain name is required for the mapped project') + domain_id = public.M('domain').where('name=? AND pid=?',(domain_arr[0],project_id)).getField('id') + if not domain_id: + return public.return_error('The specified domain name does not exist') + public.M('domain').where('id=?',(domain_id,)).delete() + + if get.domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].remove(get.domain) + if get.domain+":80" in project_find['project_config']['domains']: + project_find['project_config']['domains'].remove(get.domain + ":80") + + public.M('sites').where('id=?',(project_id,)).save('project_config',json.dumps(project_find['project_config'])) + public.WriteLog(self._log_name,'From project: [{}], delete domain name [{}]'.format(get.project_name,get.domain)) + self.set_config(get.project_name) + return public.return_data(True,'Domain name deleted successfully') + + + def bind_extranet(self,get): + ''' + @name 绑定外网 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + if not public.is_apache_nginx(): return public.return_error('Please install Nginx or Apache first') + project_name = get.project_name.strip() + project_find = self.get_project_find(project_name) + if not project_find: return public.return_error('Item does not exist') + if not project_find['project_config']['domains']: return public.return_error('Please add at least one domain name in the [Domain Management] option') + project_find['project_config']['bind_extranet'] = 1 + public.M('sites').where("id=?",(project_find['id'],)).setField('project_config',json.dumps(project_find['project_config'])) + self.set_config(project_name) + public.WriteLog(self._log_name,'Node project{}, enable mapping'.format(project_name)) + return public.return_data(True,'Enable the mapping successfully') + + + def set_config(self,project_name): + ''' + @name 设置项目配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + if not project_find['project_config']: return False + if not project_find['project_config']['bind_extranet']: return False + if not project_find['project_config']['domains']: return False + self.set_nginx_config(project_find) + self.set_apache_config(project_find) + public.serviceReload() + return True + + def clear_config(self,project_name): + ''' + @name 清除项目配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + self.clear_nginx_config(project_find) + self.clear_apache_config(project_find) + public.serviceReload() + return True + + def clear_apache_config(self,project_find): + ''' + @name 清除apache配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + config_file = "{}/apache/node_{}.conf".format(self._vhost_path,project_name) + if os.path.exists(config_file): + os.remove(config_file) + return True + + + def clear_nginx_config(self,project_find): + ''' + @name 清除nginx配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + config_file = "{}/nginx/node_{}.conf".format(self._vhost_path,project_name) + if os.path.exists(config_file): + os.remove(config_file) + rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path = self._panel_path,project_name = project_name) + if os.path.exists(rewrite_file): + os.remove(rewrite_file) + return True + + + def set_nginx_config(self,project_find): + ''' + @name 设置Nginx配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + ports = [] + domains = [] + + for d in project_find['project_config']['domains']: + domain_tmp = d.split(':') + if len(domain_tmp) == 1: domain_tmp.append(80) + if not int(domain_tmp[1]) in ports: + ports.append(int(domain_tmp[1])) + if not domain_tmp[0] in domains: + domains.append(domain_tmp[0]) + listen_ipv6 = public.listen_ipv6() + listen_ports = '' + for p in ports: + listen_ports += " listen {};\n".format(p) + if listen_ipv6: + listen_ports += " listen [::]:{};\n".format(p) + listen_ports = listen_ports.strip() + + is_ssl,is_force_ssl = self.exists_nginx_ssl(project_name) + ssl_config = '' + if is_ssl: + listen_ports += "\n listen 443 ssl http2;" + if listen_ipv6: listen_ports += "\n listen [::]:443 ssl http2;" + + ssl_config = '''ssl_certificate {vhost_path}/cert/{priject_name}/fullchain.pem; + ssl_certificate_key {vhost_path}/cert/{priject_name}/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; + error_page 497 https://$host$request_uri;'''.format(vhost_path = self._vhost_path,priject_name = project_name) + + if is_force_ssl: + ssl_config += ''' + #HTTP_TO_HTTPS_START + if ($server_port !~ 443){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END''' + + config_file = "{}/nginx/node_{}.conf".format(self._vhost_path,project_name) + template_file = "{}/template/nginx/node_http.conf".format(self._vhost_path) + + config_body = public.readFile(template_file) + config_body = config_body.format( + site_path = project_find['path'], + domains = ' '.join(domains), + project_name = project_name, + panel_path = self._panel_path, + log_path = public.get_logs_path(), + url = 'http://127.0.0.1:{}'.format(project_find['project_config']['port']), + host = '$host', + listen_ports = listen_ports, + ssl_config = ssl_config + ) + + # # 恢复旧的SSL配置 + # ssl_config = self.get_nginx_ssl_config(project_name) + # if ssl_config: + # config_body.replace('#error_page 404/404.html;',ssl_config) + + + rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path = self._panel_path,project_name = project_name) + if not os.path.exists(rewrite_file): public.writeFile(rewrite_file,'# Please fill in the URLrewrite or custom NGINX config here\n') + public.writeFile(config_file,config_body) + return True + + def get_nginx_ssl_config(self,project_name): + ''' + @name 获取项目Nginx SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return string + ''' + result = '' + config_file = "{}/nginx/node_{}".format(self._vhost_path,project_name) + if not os.path.exists(config_file): + return result + + config_body = public.readFile(config_file) + if not config_body: + return result + if config_body.find('ssl_certificate') == -1: + return result + + ssl_body = re.search("#SSL-START(.|\n)+#SSL-END",config_body) + if not ssl_body: return result + result = ssl_body.group() + return result + + def exists_nginx_ssl(self,project_name): + ''' + @name 判断项目是否配置Nginx SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return tuple + ''' + config_file = "{}/nginx/node_{}.conf".format(public.get_vhost_path(),project_name) + if not os.path.exists(config_file): + return False,False + + config_body = public.readFile(config_file) + if not config_body: + return False,False + + is_ssl,is_force_ssl = False,False + if config_body.find('ssl_certificate') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl,is_force_ssl + + def exists_apache_ssl(self,project_name): + ''' + @name 判断项目是否配置Apache SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + config_file = "{}/apache/node_{}.conf".format(public.get_vhost_path(),project_name) + if not os.path.exists(config_file): + return False,False + + config_body = public.readFile(config_file) + if not config_body: + return False,False + + is_ssl,is_force_ssl = False,False + if config_body.find('SSLCertificateFile') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl,is_force_ssl + + def set_apache_config(self,project_find): + ''' + @name 设置Apache配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + + # 处理域名和端口 + ports = [] + domains = [] + for d in project_find['project_config']['domains']: + domain_tmp = d.split(':') + if len(domain_tmp) == 1: domain_tmp.append(80) + if not int(domain_tmp[1]) in ports: + ports.append(int(domain_tmp[1])) + if not domain_tmp[0] in domains: + domains.append(domain_tmp[0]) + + + config_file = "{}/apache/node_{}.conf".format(self._vhost_path,project_name) + template_file = "{}/template/apache/node_http.conf".format(self._vhost_path) + config_body = public.readFile(template_file) + apache_config_body = '' + + # 旧的配置文件是否配置SSL + is_ssl,is_force_ssl = self.exists_apache_ssl(project_name) + if is_ssl: + if not 443 in ports: ports.append(443) + + from panelSite import panelSite + s = panelSite() + + # 根据端口列表生成配置 + for p in ports: + # 生成SSL配置 + ssl_config = '' + if p == 443 and is_ssl: + ssl_key_file = "{vhost_path}/cert/{project_name}/privkey.pem".format(project_name = project_name,vhost_path = public.get_vhost_path()) + if not os.path.exists(ssl_key_file): continue # 不存在证书文件则跳过 + ssl_config = '''#SSL + SSLEngine On + SSLCertificateFile {vhost_path}/cert/{project_name}/fullchain.pem + SSLCertificateKeyFile {vhost_path}/cert/{project_name}/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 + SSLProtocol All -SSLv2 -SSLv3 -TLSv1 + SSLHonorCipherOrder On'''.format(project_name = project_name,vhost_path = public.get_vhost_path()) + else: + if is_force_ssl: + ssl_config = '''#HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + + #HTTP_TO_HTTPS_END''' + + # 生成vhost主体配置 + apache_config_body += config_body.format( + site_path = project_find['path'], + server_name = '{}.{}'.format(p,project_name), + domains = ' '.join(domains), + log_path = public.get_logs_path(), + server_admin = 'admin@{}'.format(project_name), + url = 'http://127.0.0.1:{}'.format(project_find['project_config']['port']), + port = p, + ssl_config = ssl_config, + project_name = project_name + ) + apache_config_body += "\n" + + # 添加端口到主配置文件 + if not p in [80]: + s.apacheAddPort(p) + + # 写.htaccess + rewrite_file = "{}/.htaccess".format(project_find['path']) + if not os.path.exists(rewrite_file): public.writeFile(rewrite_file,'# Please fill in the URLrewrite rules or custom Apache config here\n') + + # 写配置文件 + public.writeFile(config_file,apache_config_body) + return True + + + def unbind_extranet(self,get): + ''' + @name 解绑外网 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_name = get.project_name.strip() + self.clear_config(project_name) + public.serviceReload() + project_find = self.get_project_find(project_name) + project_find['project_config']['bind_extranet'] = 0 + public.M('sites').where("id=?",(project_find['id'],)).setField('project_config',json.dumps(project_find['project_config'])) + public.WriteLog(self._log_name,'Node project {}, disable the mapping'.format(project_name)) + return public.return_data(True,'Disabled successfully') + + + def get_project_pids(self,get = None,pid = None): + ''' + @name 获取项目进程pid列表 + @author hwliang<2021-08-10> + @param pid: string<项目pid> + @return list + ''' + if get: pid = int(get.pid) + if not self._pids: self._pids = psutil.pids() + project_pids = [] + + for i in self._pids: + try: + p = psutil.Process(i) + except: continue + if p.ppid() == pid: + if i in project_pids: continue + if p.name() in ['bash']: continue + project_pids.append(i) + + other_pids = [] + for i in project_pids: + other_pids += self.get_project_pids(pid=i) + if os.path.exists('/proc/{}'.format(pid)): + project_pids.append(pid) + + all_pids = list(set(project_pids + other_pids)) + if not all_pids: + all_pids = self.get_other_pids(pid) + return sorted(all_pids) + + def get_other_pids(self,pid): + ''' + @name 获取其他进程pid列表 + @author hwliang<2021-08-10> + @param pid: string<项目pid> + @return list + ''' + plugin_name = None + for pid_name in os.listdir(self._node_pid_path): + pid_file = '{}/{}'.format(self._node_pid_path,pid_name) + s_pid = int(public.readFile(pid_file)) + if pid == s_pid: + plugin_name = pid_name[:-4] + break + project_find = self.get_project_find(plugin_name) + if not project_find: return [] + if not self._pids: self._pids = psutil.pids() + all_pids = [] + for i in self._pids: + try: + p = psutil.Process(i) + if p.cwd() == project_find['path'] and p.username() == project_find['project_config']['run_user']: + if p.name() in ['node','npm','pm2']: + all_pids.append(i) + except: continue + return all_pids + + def kill_pids(self,get=None,pids = None): + ''' + @name 结束进程列表 + @author hwliang<2021-08-10> + @param pids: string<进程pid列表> + @return dict + ''' + if get: pids = get.pids + if not pids: return public.return_data(True, 'No process') + pids = sorted(pids,reverse=True) + for i in pids: + try: + p = psutil.Process(i) + p.kill() + except: + pass + return public.return_data(True, 'The process has all ended') + + + + + def start_project(self,get): + ''' + @name 启动项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if os.path.exists(pid_file): + self.stop_project(get) + + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('Item does not exist') + + if not os.path.exists(project_find['path']): + error_msg = 'Startup failed, Nodejs project {}, running directory {} does not exist!'.format(get.project_name,project_find['path']) + public.WriteLog(self._log_name,error_msg) + return public.return_error(error_msg) + + # 是否安装依赖模块? + package_file = "{}/package.json".format(project_find['path']) + package_info = {} + if os.path.exists(package_file): + node_modules_path = "{}/node_modules".format(project_find['path']) + if not os.path.exists(node_modules_path): + return public.return_error('Please go to the [Module] and click [One-key install] to install the module dependencies!') + package_info = json.loads(public.readFile(package_file)) + if not package_info: package_info['scripts'] = {} + if 'scripts' not in package_info: package_info['scripts'] = {} + try: + scripts_keys = package_info['scripts'].keys() + except: + scripts_keys = [] + + + # 前置准备 + nodejs_version = project_find['project_config']['nodejs_version'] + node_bin = self.get_node_bin(nodejs_version) + npm_bin = self.get_npm_bin(nodejs_version) + project_script = project_find['project_config']['project_script'].strip() + log_file = "{}/{}.log".format(self._node_logs_path,get.project_name) + if not project_script: return public.return_error('No startup script configured') + + last_env = self.get_last_env(nodejs_version,project_find['path']) + + # 生成启动脚本 + if os.path.exists(project_script): + start_cmd = '''{last_env} +cd {project_cwd} +nohup {node_bin} {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + node_bin = node_bin, + project_script = project_script, + log_file = log_file, + pid_file = pid_file, + last_env = last_env +) + elif project_script in scripts_keys: + start_cmd = '''{last_env} +cd {project_cwd} +nohup {npm_bin} run {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + npm_bin = npm_bin, + project_script = project_script, + pid_file = pid_file, + log_file = log_file, + last_env = last_env +) + else: + start_cmd = '''{last_env} +cd {project_cwd} +nohup {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + project_script = project_script, + pid_file = pid_file, + log_file = log_file, + last_env = last_env +) + script_file = "{}/{}.sh".format(self._node_run_scripts,get.project_name) + + # 写入启动脚本 + public.writeFile(script_file,start_cmd) + if os.path.exists(pid_file): os.remove(pid_file) + + # 处理前置权限 + public.ExecShell("chown -R {user}:{user} {project_cwd}".format(user=project_find['project_config']['run_user'],project_cwd=project_find['path'])) + public.ExecShell("chown -R www:www {}/vhost".format(self._nodejs_path)) + public.ExecShell("chmod 755 {} {} {}".format(self._nodejs_path,public.get_setup_path(),'/www')) + public.set_own(script_file,project_find['project_config']['run_user'],project_find['project_config']['run_user']) + public.set_mode(script_file,755) + + # 执行脚本文件 + p = public.ExecShell("bash {}".format(script_file),user=project_find['project_config']['run_user']) + time.sleep(1) + if not os.path.exists(pid_file): + p = '\n'.join(p) + if p.find('[Errno 0]') != -1: + if os.path.exists('{}/bt_security'.format(public.get_plugin_path())): + return public.return_error('The start command was intercepted by [Fort Tower Defense Privilege], please turn off {} user protection'.format(project_find['project_config']['run_user'])) + return public.return_error('The startup command was intercepted by unknown security software, please check the installation software log') + return public.return_error('failed to activate
          {}
          '.format(p)) + + # 获取PID + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: + if os.path.exists(pid_file): os.remove(pid_file) + return public.return_error('failed to activate
          {}'.format(public.GetNumLines(log_file,20))) + + return public.return_data(True, 'Successfully started', pids) + + + def stop_project(self,get): + ''' + @name 停止项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if not os.path.exists(pid_file): return public.return_error('Project did not start') + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: return public.return_error('Project did not start') + self.kill_pids(pids=pids) + if os.path.exists(pid_file): os.remove(pid_file) + return public.return_data(True, 'Stopped successfully') + + def restart_project(self,get): + ''' + @name 重启项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + res = self.stop_project(get) + if not res['status']: return res + res = self.start_project(get) + if not res['status']: return res + return public.return_data(True, 'Successful restart') + + def get_project_log(self,get): + ''' + @name 获取项目日志 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + log_file = "{}/{}.log".format(self._node_logs_path,get.project_name) + if not os.path.exists(log_file): return public.return_error('Log file does not exist') + return public.GetNumLines(log_file,200) + + + def get_project_load_info(self,get = None,project_name = None): + ''' + @name 获取项目负载信息 + @author hwliang<2021-08-12> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + if get: project_name = get.project_name.strip() + load_info = {} + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return load_info + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: return load_info + for i in pids: + process_info = self.get_process_info_by_pid(i) + if process_info: load_info[i] = process_info + return load_info + + + def object_to_dict(self,obj): + ''' + @name 将对象转换为字典 + @author hwliang<2021-08-09> + @param obj + @return dict + ''' + result = {} + for name in dir(obj): + value = getattr(obj, name) + if not name.startswith('__') and not callable(value) and not name.startswith('_'): result[name] = value + return result + + + def list_to_dict(self,data): + ''' + @name 将列表转换为字典 + @author hwliang<2021-08-09> + @param data + @return dict + ''' + result = [] + for s in data: + result.append(self.object_to_dict(s)) + return result + + + def get_connects(self,pid): + ''' + @name 获取进程连接信息 + @author hwliang<2021-08-09> + @param pid + @return dict + ''' + connects = 0 + try: + if pid == 1: return connects + tp = '/proc/' + str(pid) + '/fd/' + if not os.path.exists(tp): return connects + for d in os.listdir(tp): + fname = tp + d + if os.path.islink(fname): + l = os.readlink(fname) + if l.find('socket:') != -1: connects += 1 + except:pass + return connects + + + def format_connections(self,connects): + ''' + @name 获取进程网络连接信息 + @author hwliang<2021-08-09> + @param connects + @return list + ''' + result = [] + for i in connects: + raddr = i.raddr + if not i.raddr: + raddr = ('',0) + laddr = i.laddr + if not i.laddr: + laddr = ('',0) + result.append({ + "fd": i.fd, + "family": i.family, + "local_addr": laddr[0], + "local_port": laddr[1], + "client_addr": raddr[0], + "client_rport": raddr[1], + "status": i.status + }) + return result + + + def get_process_info_by_pid(self,pid): + ''' + @name 获取进程信息 + @author hwliang<2021-08-12> + @param pid: int<进程id> + @return dict + ''' + process_info = {} + try: + if not os.path.exists('/proc/{}'.format(pid)): return process_info + p = psutil.Process(pid) + status_ps = {'sleeping':'Sleeping','running':'Running'} + with p.oneshot(): + p_mem = p.memory_full_info() + if p_mem.uss + p_mem.rss + p_mem.pss + p_mem.data == 0: return process_info + p_state = p.status() + if p_state in status_ps: p_state = status_ps[p_state] + # process_info['exe'] = p.exe() + process_info['name'] = p.name() + process_info['pid'] = pid + process_info['ppid'] = p.ppid() + process_info['create_time'] = int(p.create_time()) + process_info['status'] = p_state + process_info['user'] = p.username() + process_info['memory_used'] = p_mem.uss + process_info['cpu_percent'] = self.get_cpu_precent(p) + process_info['io_write_bytes'],process_info['io_read_bytes'] = self.get_io_speed(p) + process_info['connections'] = self.format_connections(p.connections()) + process_info['connects'] = self.get_connects(pid) + process_info['open_files'] = self.list_to_dict(p.open_files()) + process_info['threads'] = p.num_threads() + process_info['exe'] = ' '.join(p.cmdline()) + return process_info + except: + return process_info + + + def get_io_speed(self,p): + ''' + @name 获取磁盘IO速度 + @author hwliang<2021-08-12> + @param p: Process<进程对像> + @return list + ''' + skey = "io_speed_{}".format(p.pid) + old_pio = cache.get(skey) + pio = p.io_counters() + if not old_pio: + cache.set(skey,[pio,time.time()],3600) + # time.sleep(0.1) + old_pio = cache.get(skey) + pio = p.io_counters() + + old_write_bytes = old_pio[0].write_bytes + old_read_bytes = old_pio[0].read_bytes + old_time = old_pio[1] + + new_time = time.time() + write_bytes = pio.write_bytes + read_bytes = pio.read_bytes + + cache.set(skey,[pio,new_time],3600) + + write_speed = int((write_bytes - old_write_bytes) / (new_time - old_time)) + read_speed = int((read_bytes - old_read_bytes) / (new_time - old_time)) + + return write_speed,read_speed + + + + + + def get_cpu_precent(self,p): + ''' + @name 获取进程cpu使用率 + @author hwliang<2021-08-09> + @param p: Process<进程对像> + @return dict + ''' + skey = "cpu_pre_{}".format(p.pid) + old_cpu_times = cache.get(skey) + + process_cpu_time = self.get_process_cpu_time(p.cpu_times()) + if not old_cpu_times: + cache.set(skey,[process_cpu_time,time.time()],3600) + # time.sleep(0.1) + old_cpu_times = cache.get(skey) + process_cpu_time = self.get_process_cpu_time(p.cpu_times()) + + old_process_cpu_time = old_cpu_times[0] + old_time = old_cpu_times[1] + new_time = time.time() + cache.set(skey,[process_cpu_time,new_time],3600) + percent = round(100.00 * (process_cpu_time - old_process_cpu_time) / (new_time - old_time) / psutil.cpu_count(),2) + return percent + + + def get_process_cpu_time(self,cpu_times): + cpu_time = 0.00 + for s in cpu_times: cpu_time += s + return cpu_time + + + def get_project_run_state(self,get = None,project_name = None): + ''' + @name 获取项目运行状态 + @author hwliang<2021-08-12> + @param get{ + project_name: string<项目名称> + } + @param project_name 项目名称 + @return bool + ''' + if get: project_name = get.project_name.strip() + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return False + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: return False + return True + + def get_project_find(self,project_name): + ''' + @name 获取指定项目配置 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',project_name)).find() + if not project_info: return False + project_info['project_config'] = json.loads(project_info['project_config']) + return project_info + + + def get_project_info(self,get): + ''' + @name 获取指定项目信息 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',get.project_name)).find() + if not project_info: return public.return_error('The specified item does not exist!') + project_info = self.get_project_stat(project_info) + return project_info + + + def get_project_stat(self,project_info): + ''' + @name 获取项目状态信息 + @author hwliang<2021-08-09> + @param project_info 项目信息 + @return list + ''' + project_info['project_config'] = json.loads(project_info['project_config']) + project_info['run'] = self.get_project_run_state(project_name = project_info['name']) + project_info['load_info'] = self.get_project_load_info(project_name = project_info['name']) + project_info['ssl'] = self.get_ssl_end_date(project_name = project_info['name']) + project_info['listen'] = [] + project_info['listen_ok'] = True + if project_info['load_info']: + for pid in project_info['load_info'].keys(): + for conn in project_info['load_info'][pid]['connections']: + if not conn['status'] == 'LISTEN': continue + if not conn['local_port'] in project_info['listen']: + project_info['listen'].append(conn['local_port']) + if project_info['listen']: + project_info['listen_ok'] = project_info['project_config']['port'] in project_info['listen'] + return project_info + + + + def get_project_state(self,project_name): + ''' + @name 获取项目状态 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',project_name)).find() + if not project_info: return False + return project_info['status'] + + def get_project_listen(self,project_name): + ''' + @name 获取项目监听端口 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + project_config = json.loads(public.M('sites').where('name=?',project_name).getField('project_config')) + if 'listen_port' in project_config: return project_config['listen_port'] + return False + + + def set_project_listen(self,get): + ''' + @name 设置项目监听端口(请设置与实际端口相符的,仅在自动获取不正确时使用) + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + port: int<端口> + } + @return dict + ''' + project_config = json.loads(public.M('sites').where('name=?',get.project_name).getField('project_config')) + project_config['listen_port'] = get.port + public.M('sites').where('name=?',get.project_name).save('project_config',json.dumps(project_config)) + public.WriteLog(self._log_name, 'Modify the port of the project ['+get.project_name+'] to ['+get.port+']') + return public.return_data(True,'Set successfully') + + + def set_project_nodejs_version(self,get): + ''' + @name 设置nodejs版本 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + nodejs_version: string + } + @return dict + ''' + + project_config = json.loads(public.M('sites').where('name=?',get.project_name).getField('project_config')) + project_config['nodejs_version'] = get.nodejs_version + public.M('sites').where('name=?',get.project_name).save('project_config',json.dumps(project_config)) + public.WriteLog(self._log_name, 'Modify the nodejs version of the project ['+get.project_name+'] to ['+get.nodejs_version+']') + return public.return_data(True,'Set successfully') + + def get_project_nodejs_version(self,project_name): + ''' + @name 获取nodejs版本 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return string + ''' + + project_config = json.loads(public.M('sites').where('name=?',project_name).getField('project_config')) + if 'nodejs_version' in project_config: return project_config['nodejs_version'] + return False + + + def check_port_is_used(self,port,sock=False): + ''' + @name 检查端口是否被占用 + @author hwliang<2021-08-09> + @param port: int<端口> + @return bool + ''' + if not isinstance(port,int): port = int(port) + if port == 0: return False + project_list = public.M('sites').where('status=? AND project_type=?',(1,'Node')).field('name,path,project_config').select() + for project_find in project_list: + project_config = json.loads(project_find['project_config']) + if not 'port' in project_config: continue + if int(project_config['port']) == port: return True + if sock: return False + return public.check_tcp('127.0.0.1',port) + + def get_project_run_state_byaotu(self,project_name): + ''' + @name 获取项目运行状态 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return False + pid = public.readFile(pid_file) + pids = self.get_project_pids(pid=pid) + if not pids: return False + return True + + def auto_run(self): + ''' + @name 自动启动所有项目 + @author hwliang<2021-08-09> + @return bool + ''' + project_list = public.M('sites').where('project_type=?',('Node',)).field('name,path,project_config').select() + get= public.dict_obj() + success_count = 0 + error_count = 0 + for project_find in project_list: + try: + project_config = json.loads(project_find['project_config']) + if project_config['is_power_on'] in [0,False,'0',None]: continue + project_name = project_find['name'] + project_state = self.get_project_run_state(project_name=project_name) + if not project_state: + get.project_name = project_name + result = self.start_project(get) + if not result['status']: + error_count += 1 + error_msg = 'Automatically start Nodej project ['+project_name+'] failed!' + public.WriteLog(self._log_name, error_msg) + public.print_log(error_msg + ", " + result['error_msg'],'ERROR') + else: + success_count += 1 + success_msg = 'Automatically start the Nodej project ['+project_name+'] successfully!' + public.WriteLog(self._log_name, success_msg) + public.print_log(success_msg,'INFO') + except: + error_count += 1 + public.print_log(public.get_error_info(),'ERROR') + if (success_count + error_count) < 1: return False + dene_msg = 'A total of {} Nodejs projects need to be started, {} successfully and {} failed'.format(success_count + error_count,success_count,error_count) + public.WriteLog(self._log_name, dene_msg) + public.print_log(dene_msg,'INFO') + return True diff --git a/class/ptyprocess/__init__.py b/class/ptyprocess/__init__.py new file mode 100644 index 00000000..e633d0cd --- /dev/null +++ b/class/ptyprocess/__init__.py @@ -0,0 +1,4 @@ +"""Run a subprocess in a pseudo terminal""" +from .ptyprocess import PtyProcess, PtyProcessUnicode, PtyProcessError + +__version__ = '0.6.0' diff --git a/class/ptyprocess/_fork_pty.py b/class/ptyprocess/_fork_pty.py new file mode 100644 index 00000000..a8d05fe5 --- /dev/null +++ b/class/ptyprocess/_fork_pty.py @@ -0,0 +1,78 @@ +"""Substitute for the forkpty system call, to support Solaris. +""" +import os +import errno + +from pty import (STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, CHILD) +from .util import PtyProcessError + +def fork_pty(): + '''This implements a substitute for the forkpty system call. This + should be more portable than the pty.fork() function. Specifically, + this should work on Solaris. + + Modified 10.06.05 by Geoff Marshall: Implemented __fork_pty() method to + resolve the issue with Python's pty.fork() not supporting Solaris, + particularly ssh. Based on patch to posixmodule.c authored by Noah + Spurrier:: + + http://mail.python.org/pipermail/python-dev/2003-May/035281.html + + ''' + + parent_fd, child_fd = os.openpty() + if parent_fd < 0 or child_fd < 0: + raise OSError("os.openpty() failed") + + pid = os.fork() + if pid == CHILD: + # Child. + os.close(parent_fd) + pty_make_controlling_tty(child_fd) + + os.dup2(child_fd, STDIN_FILENO) + os.dup2(child_fd, STDOUT_FILENO) + os.dup2(child_fd, STDERR_FILENO) + + else: + # Parent. + os.close(child_fd) + + return pid, parent_fd + +def pty_make_controlling_tty(tty_fd): + '''This makes the pseudo-terminal the controlling tty. This should be + more portable than the pty.fork() function. Specifically, this should + work on Solaris. ''' + + child_name = os.ttyname(tty_fd) + + # Disconnect from controlling tty, if any. Raises OSError of ENXIO + # if there was no controlling tty to begin with, such as when + # executed by a cron(1) job. + try: + fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY) + os.close(fd) + except OSError as err: + if err.errno != errno.ENXIO: + raise + + os.setsid() + + # Verify we are disconnected from controlling tty by attempting to open + # it again. We expect that OSError of ENXIO should always be raised. + try: + fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY) + os.close(fd) + raise PtyProcessError("OSError of errno.ENXIO should be raised.") + except OSError as err: + if err.errno != errno.ENXIO: + raise + + # Verify we can open child pty. + fd = os.open(child_name, os.O_RDWR) + os.close(fd) + + # Verify we now have a controlling tty. + fd = os.open("/dev/tty", os.O_WRONLY) + os.close(fd) diff --git a/class/ptyprocess/ptyprocess.py b/class/ptyprocess/ptyprocess.py new file mode 100644 index 00000000..78d19fdf --- /dev/null +++ b/class/ptyprocess/ptyprocess.py @@ -0,0 +1,842 @@ +import codecs +import errno +import fcntl +import io +import os +import pty +import resource +import signal +import struct +import sys +import termios +import time + +try: + import builtins # Python 3 +except ImportError: + import __builtin__ as builtins # Python 2 + +# Constants +from pty import (STDIN_FILENO, CHILD) + +from .util import which, PtyProcessError + +_platform = sys.platform.lower() + +# Solaris uses internal __fork_pty(). All others use pty.fork(). +_is_solaris = ( + _platform.startswith('solaris') or + _platform.startswith('sunos')) + +if _is_solaris: + use_native_pty_fork = False + from . import _fork_pty +else: + use_native_pty_fork = True + +PY3 = sys.version_info[0] >= 3 + +if PY3: + def _byte(i): + return bytes([i]) +else: + def _byte(i): + return chr(i) + + class FileNotFoundError(OSError): pass + class TimeoutError(OSError): pass + +_EOF, _INTR = None, None + +def _make_eof_intr(): + """Set constants _EOF and _INTR. + + This avoids doing potentially costly operations on module load. + """ + global _EOF, _INTR + if (_EOF is not None) and (_INTR is not None): + return + + # inherit EOF and INTR definitions from controlling process. + try: + from termios import VEOF, VINTR + fd = None + for name in 'stdin', 'stdout': + stream = getattr(sys, '__%s__' % name, None) + if stream is None or not hasattr(stream, 'fileno'): + continue + try: + fd = stream.fileno() + except ValueError: + continue + if fd is None: + # no fd, raise ValueError to fallback on CEOF, CINTR + raise ValueError("No stream has a fileno") + intr = ord(termios.tcgetattr(fd)[6][VINTR]) + eof = ord(termios.tcgetattr(fd)[6][VEOF]) + except (ImportError, OSError, IOError, ValueError, termios.error): + # unless the controlling process is also not a terminal, + # such as cron(1), or when stdin and stdout are both closed. + # Fall-back to using CEOF and CINTR. There + try: + from termios import CEOF, CINTR + (intr, eof) = (CINTR, CEOF) + except ImportError: + # ^C, ^D + (intr, eof) = (3, 4) + + _INTR = _byte(intr) + _EOF = _byte(eof) + +# setecho and setwinsize are pulled out here because on some platforms, we need +# to do this from the child before we exec() + +def _setecho(fd, state): + errmsg = 'setecho() may not be called on this platform (it may still be possible to enable/disable echo when spawning the child process)' + + try: + attr = termios.tcgetattr(fd) + except termios.error as err: + if err.args[0] == errno.EINVAL: + raise IOError(err.args[0], '%s: %s.' % (err.args[1], errmsg)) + raise + + if state: + attr[3] = attr[3] | termios.ECHO + else: + attr[3] = attr[3] & ~termios.ECHO + + try: + # I tried TCSADRAIN and TCSAFLUSH, but these were inconsistent and + # blocked on some platforms. TCSADRAIN would probably be ideal. + termios.tcsetattr(fd, termios.TCSANOW, attr) + except IOError as err: + if err.args[0] == errno.EINVAL: + raise IOError(err.args[0], '%s: %s.' % (err.args[1], errmsg)) + raise + +def _setwinsize(fd, rows, cols): + # Some very old platforms have a bug that causes the value for + # termios.TIOCSWINSZ to be truncated. There was a hack here to work + # around this, but it caused problems with newer platforms so has been + # removed. For details see https://github.com/pexpect/pexpect/issues/39 + TIOCSWINSZ = getattr(termios, 'TIOCSWINSZ', -2146929561) + # Note, assume ws_xpixel and ws_ypixel are zero. + s = struct.pack('HHHH', rows, cols, 0, 0) + fcntl.ioctl(fd, TIOCSWINSZ, s) + +class PtyProcess(object): + '''This class represents a process running in a pseudoterminal. + + The main constructor is the :meth:`spawn` classmethod. + ''' + string_type = bytes + if PY3: + linesep = os.linesep.encode('ascii') + crlf = '\r\n'.encode('ascii') + + @staticmethod + def write_to_stdout(b): + try: + return sys.stdout.buffer.write(b) + except AttributeError: + # If stdout has been replaced, it may not have .buffer + return sys.stdout.write(b.decode('ascii', 'replace')) + else: + linesep = os.linesep + crlf = '\r\n' + write_to_stdout = sys.stdout.write + + encoding = None + + argv = None + env = None + launch_dir = None + + def __init__(self, pid, fd): + _make_eof_intr() # Ensure _EOF and _INTR are calculated + self.pid = pid + self.fd = fd + readf = io.open(fd, 'rb', buffering=0) + writef = io.open(fd, 'wb', buffering=0, closefd=False) + self.fileobj = io.BufferedRWPair(readf, writef) + + self.terminated = False + self.closed = False + self.exitstatus = None + self.signalstatus = None + # status returned by os.waitpid + self.status = None + self.flag_eof = False + # Used by close() to give kernel time to update process status. + # Time in seconds. + self.delayafterclose = 0.1 + # Used by terminate() to give kernel time to update process status. + # Time in seconds. + self.delayafterterminate = 0.1 + + @classmethod + def spawn( + cls, argv, cwd=None, env=None, echo=True, preexec_fn=None, + dimensions=(24, 80), pass_fds=()): + '''Start the given command in a child process in a pseudo terminal. + + This does all the fork/exec type of stuff for a pty, and returns an + instance of PtyProcess. + + If preexec_fn is supplied, it will be called with no arguments in the + child process before exec-ing the specified command. + It may, for instance, set signal handlers to SIG_DFL or SIG_IGN. + + Dimensions of the psuedoterminal used for the subprocess can be + specified as a tuple (rows, cols), or the default (24, 80) will be used. + + By default, all file descriptors except 0, 1 and 2 are closed. This + behavior can be overridden with pass_fds, a list of file descriptors to + keep open between the parent and the child. + ''' + # Note that it is difficult for this method to fail. + # You cannot detect if the child process cannot start. + # So the only way you can tell if the child process started + # or not is to try to read from the file descriptor. If you get + # EOF immediately then it means that the child is already dead. + # That may not necessarily be bad because you may have spawned a child + # that performs some task; creates no stdout output; and then dies. + + if not isinstance(argv, (list, tuple)): + raise TypeError("Expected a list or tuple for argv, got %r" % argv) + + # Shallow copy of argv so we can modify it + argv = argv[:] + command = argv[0] + + command_with_path = which(command) + if command_with_path is None: + raise FileNotFoundError('The command was not found or was not ' + + 'executable: %s.' % command) + command = command_with_path + argv[0] = command + + # [issue #119] To prevent the case where exec fails and the user is + # stuck interacting with a python child process instead of whatever + # was expected, we implement the solution from + # http://stackoverflow.com/a/3703179 to pass the exception to the + # parent process + + # [issue #119] 1. Before forking, open a pipe in the parent process. + exec_err_pipe_read, exec_err_pipe_write = os.pipe() + + if use_native_pty_fork: + pid, fd = pty.fork() + else: + # Use internal fork_pty, for Solaris + pid, fd = _fork_pty.fork_pty() + + # Some platforms must call setwinsize() and setecho() from the + # child process, and others from the master process. We do both, + # allowing IOError for either. + + if pid == CHILD: + # set window size + try: + _setwinsize(STDIN_FILENO, *dimensions) + except IOError as err: + if err.args[0] not in (errno.EINVAL, errno.ENOTTY): + raise + + # disable echo if spawn argument echo was unset + if not echo: + try: + _setecho(STDIN_FILENO, False) + except (IOError, termios.error) as err: + if err.args[0] not in (errno.EINVAL, errno.ENOTTY): + raise + + # [issue #119] 3. The child closes the reading end and sets the + # close-on-exec flag for the writing end. + os.close(exec_err_pipe_read) + fcntl.fcntl(exec_err_pipe_write, fcntl.F_SETFD, fcntl.FD_CLOEXEC) + + # Do not allow child to inherit open file descriptors from parent, + # with the exception of the exec_err_pipe_write of the pipe + # and pass_fds. + # Impose ceiling on max_fd: AIX bugfix for users with unlimited + # nofiles where resource.RLIMIT_NOFILE is 2^63-1 and os.closerange() + # occasionally raises out of range error + max_fd = min(1048576, resource.getrlimit(resource.RLIMIT_NOFILE)[0]) + spass_fds = sorted(set(pass_fds) | {exec_err_pipe_write}) + for pair in zip([2] + spass_fds, spass_fds + [max_fd]): + os.closerange(pair[0]+1, pair[1]) + + if cwd is not None: + os.chdir(cwd) + + if preexec_fn is not None: + try: + preexec_fn() + except Exception as e: + ename = type(e).__name__ + tosend = '{}:0:{}'.format(ename, str(e)) + if PY3: + tosend = tosend.encode('utf-8') + + os.write(exec_err_pipe_write, tosend) + os.close(exec_err_pipe_write) + os._exit(1) + + try: + if env is None: + os.execv(command, argv) + else: + os.execvpe(command, argv, env) + except OSError as err: + # [issue #119] 5. If exec fails, the child writes the error + # code back to the parent using the pipe, then exits. + tosend = 'OSError:{}:{}'.format(err.errno, str(err)) + if PY3: + tosend = tosend.encode('utf-8') + os.write(exec_err_pipe_write, tosend) + os.close(exec_err_pipe_write) + os._exit(os.EX_OSERR) + + # Parent + inst = cls(pid, fd) + + # Set some informational attributes + inst.argv = argv + if env is not None: + inst.env = env + if cwd is not None: + inst.launch_dir = cwd + + # [issue #119] 2. After forking, the parent closes the writing end + # of the pipe and reads from the reading end. + os.close(exec_err_pipe_write) + exec_err_data = os.read(exec_err_pipe_read, 4096) + os.close(exec_err_pipe_read) + + # [issue #119] 6. The parent reads eof (a zero-length read) if the + # child successfully performed exec, since close-on-exec made + # successful exec close the writing end of the pipe. Or, if exec + # failed, the parent reads the error code and can proceed + # accordingly. Either way, the parent blocks until the child calls + # exec. + if len(exec_err_data) != 0: + try: + errclass, errno_s, errmsg = exec_err_data.split(b':', 2) + exctype = getattr(builtins, errclass.decode('ascii'), Exception) + + exception = exctype(errmsg.decode('utf-8', 'replace')) + if exctype is OSError: + exception.errno = int(errno_s) + except: + raise Exception('Subprocess failed, got bad error data: %r' + % exec_err_data) + else: + raise exception + + try: + inst.setwinsize(*dimensions) + except IOError as err: + if err.args[0] not in (errno.EINVAL, errno.ENOTTY, errno.ENXIO): + raise + + return inst + + def __repr__(self): + clsname = type(self).__name__ + if self.argv is not None: + args = [repr(self.argv)] + if self.env is not None: + args.append("env=%r" % self.env) + if self.launch_dir is not None: + args.append("cwd=%r" % self.launch_dir) + + return "{}.spawn({})".format(clsname, ", ".join(args)) + + else: + return "{}(pid={}, fd={})".format(clsname, self.pid, self.fd) + + @staticmethod + def _coerce_send_string(s): + if not isinstance(s, bytes): + return s.encode('utf-8') + return s + + @staticmethod + def _coerce_read_string(s): + return s + + def __del__(self): + '''This makes sure that no system resources are left open. Python only + garbage collects Python objects. OS file descriptors are not Python + objects, so they must be handled explicitly. If the child file + descriptor was opened outside of this class (passed to the constructor) + then this does not close it. ''' + + if not self.closed: + # It is possible for __del__ methods to execute during the + # teardown of the Python VM itself. Thus self.close() may + # trigger an exception because os.close may be None. + try: + self.close() + # which exception, shouldn't we catch explicitly .. ? + except: + pass + + + def fileno(self): + '''This returns the file descriptor of the pty for the child. + ''' + return self.fd + + def close(self, force=True): + '''This closes the connection with the child application. Note that + calling close() more than once is valid. This emulates standard Python + behavior with files. Set force to True if you want to make sure that + the child is terminated (SIGKILL is sent if the child ignores SIGHUP + and SIGINT). ''' + if not self.closed: + self.flush() + self.fileobj.close() # Closes the file descriptor + # Give kernel time to update process status. + time.sleep(self.delayafterclose) + if self.isalive(): + if not self.terminate(force): + raise PtyProcessError('Could not terminate the child.') + self.fd = -1 + self.closed = True + #self.pid = None + + def flush(self): + '''This does nothing. It is here to support the interface for a + File-like object. ''' + + pass + + def isatty(self): + '''This returns True if the file descriptor is open and connected to a + tty(-like) device, else False. + + On SVR4-style platforms implementing streams, such as SunOS and HP-UX, + the child pty may not appear as a terminal device. This means + methods such as setecho(), setwinsize(), getwinsize() may raise an + IOError. ''' + + return os.isatty(self.fd) + + def waitnoecho(self, timeout=None): + '''This waits until the terminal ECHO flag is set False. This returns + True if the echo mode is off. This returns False if the ECHO flag was + not set False before the timeout. This can be used to detect when the + child is waiting for a password. Usually a child application will turn + off echo mode when it is waiting for the user to enter a password. For + example, instead of expecting the "password:" prompt you can wait for + the child to set ECHO off:: + + p = pexpect.spawn('ssh user@example.com') + p.waitnoecho() + p.sendline(mypassword) + + If timeout==None then this method to block until ECHO flag is False. + ''' + + if timeout is not None: + end_time = time.time() + timeout + while True: + if not self.getecho(): + return True + if timeout < 0 and timeout is not None: + return False + if timeout is not None: + timeout = end_time - time.time() + time.sleep(0.1) + + def getecho(self): + '''This returns the terminal echo mode. This returns True if echo is + on or False if echo is off. Child applications that are expecting you + to enter a password often set ECHO False. See waitnoecho(). + + Not supported on platforms where ``isatty()`` returns False. ''' + + try: + attr = termios.tcgetattr(self.fd) + except termios.error as err: + errmsg = 'getecho() may not be called on this platform' + if err.args[0] == errno.EINVAL: + raise IOError(err.args[0], '%s: %s.' % (err.args[1], errmsg)) + raise + + self.echo = bool(attr[3] & termios.ECHO) + return self.echo + + def setecho(self, state): + '''This sets the terminal echo mode on or off. Note that anything the + child sent before the echo will be lost, so you should be sure that + your input buffer is empty before you call setecho(). For example, the + following will work as expected:: + + p = pexpect.spawn('cat') # Echo is on by default. + p.sendline('1234') # We expect see this twice from the child... + p.expect(['1234']) # ... once from the tty echo... + p.expect(['1234']) # ... and again from cat itself. + p.setecho(False) # Turn off tty echo + p.sendline('abcd') # We will set this only once (echoed by cat). + p.sendline('wxyz') # We will set this only once (echoed by cat) + p.expect(['abcd']) + p.expect(['wxyz']) + + The following WILL NOT WORK because the lines sent before the setecho + will be lost:: + + p = pexpect.spawn('cat') + p.sendline('1234') + p.setecho(False) # Turn off tty echo + p.sendline('abcd') # We will set this only once (echoed by cat). + p.sendline('wxyz') # We will set this only once (echoed by cat) + p.expect(['1234']) + p.expect(['1234']) + p.expect(['abcd']) + p.expect(['wxyz']) + + + Not supported on platforms where ``isatty()`` returns False. + ''' + _setecho(self.fd, state) + + self.echo = state + + def read(self, size=1024): + """Read and return at most ``size`` bytes from the pty. + + Can block if there is nothing to read. Raises :exc:`EOFError` if the + terminal was closed. + + Unlike Pexpect's ``read_nonblocking`` method, this doesn't try to deal + with the vagaries of EOF on platforms that do strange things, like IRIX + or older Solaris systems. It handles the errno=EIO pattern used on + Linux, and the empty-string return used on BSD platforms and (seemingly) + on recent Solaris. + """ + try: + s = self.fileobj.read1(size) + except (OSError, IOError) as err: + if err.args[0] == errno.EIO: + # Linux-style EOF + self.flag_eof = True + raise EOFError('End Of File (EOF). Exception style platform.') + raise + if s == b'': + # BSD-style EOF (also appears to work on recent Solaris (OpenIndiana)) + self.flag_eof = True + raise EOFError('End Of File (EOF). Empty string style platform.') + + return s + + def readline(self): + """Read one line from the pseudoterminal, and return it as unicode. + + Can block if there is nothing to read. Raises :exc:`EOFError` if the + terminal was closed. + """ + try: + s = self.fileobj.readline() + except (OSError, IOError) as err: + if err.args[0] == errno.EIO: + # Linux-style EOF + self.flag_eof = True + raise EOFError('End Of File (EOF). Exception style platform.') + raise + if s == b'': + # BSD-style EOF (also appears to work on recent Solaris (OpenIndiana)) + self.flag_eof = True + raise EOFError('End Of File (EOF). Empty string style platform.') + + return s + + def _writeb(self, b, flush=True): + n = self.fileobj.write(b) + if flush: + self.fileobj.flush() + return n + + def write(self, s, flush=True): + """Write bytes to the pseudoterminal. + + Returns the number of bytes written. + """ + return self._writeb(s, flush=flush) + + def sendcontrol(self, char): + '''Helper method that wraps send() with mnemonic access for sending control + character to the child (such as Ctrl-C or Ctrl-D). For example, to send + Ctrl-G (ASCII 7, bell, '\a'):: + + child.sendcontrol('g') + + See also, sendintr() and sendeof(). + ''' + char = char.lower() + a = ord(char) + if 97 <= a <= 122: + a = a - ord('a') + 1 + byte = _byte(a) + return self._writeb(byte), byte + d = {'@': 0, '`': 0, + '[': 27, '{': 27, + '\\': 28, '|': 28, + ']': 29, '}': 29, + '^': 30, '~': 30, + '_': 31, + '?': 127} + if char not in d: + return 0, b'' + + byte = _byte(d[char]) + return self._writeb(byte), byte + + def sendeof(self): + '''This sends an EOF to the child. This sends a character which causes + the pending parent output buffer to be sent to the waiting child + program without waiting for end-of-line. If it is the first character + of the line, the read() in the user program returns 0, which signifies + end-of-file. This means to work as expected a sendeof() has to be + called at the beginning of a line. This method does not send a newline. + It is the responsibility of the caller to ensure the eof is sent at the + beginning of a line. ''' + + return self._writeb(_EOF), _EOF + + def sendintr(self): + '''This sends a SIGINT to the child. It does not require + the SIGINT to be the first character on a line. ''' + + return self._writeb(_INTR), _INTR + + def eof(self): + '''This returns True if the EOF exception was ever raised. + ''' + + return self.flag_eof + + def terminate(self, force=False): + '''This forces a child process to terminate. It starts nicely with + SIGHUP and SIGINT. If "force" is True then moves onto SIGKILL. This + returns True if the child was terminated. This returns False if the + child could not be terminated. ''' + + if not self.isalive(): + return True + try: + self.kill(signal.SIGHUP) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + self.kill(signal.SIGCONT) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + self.kill(signal.SIGINT) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + if force: + self.kill(signal.SIGKILL) + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + else: + return False + return False + except OSError: + # I think there are kernel timing issues that sometimes cause + # this to happen. I think isalive() reports True, but the + # process is dead to the kernel. + # Make one last attempt to see if the kernel is up to date. + time.sleep(self.delayafterterminate) + if not self.isalive(): + return True + else: + return False + + def wait(self): + '''This waits until the child exits. This is a blocking call. This will + not read any data from the child, so this will block forever if the + child has unread output and has terminated. In other words, the child + may have printed output then called exit(), but, the child is + technically still alive until its output is read by the parent. ''' + + if self.isalive(): + pid, status = os.waitpid(self.pid, 0) + else: + return self.exitstatus + self.exitstatus = os.WEXITSTATUS(status) + if os.WIFEXITED(status): + self.status = status + self.exitstatus = os.WEXITSTATUS(status) + self.signalstatus = None + self.terminated = True + elif os.WIFSIGNALED(status): + self.status = status + self.exitstatus = None + self.signalstatus = os.WTERMSIG(status) + self.terminated = True + elif os.WIFSTOPPED(status): # pragma: no cover + # You can't call wait() on a child process in the stopped state. + raise PtyProcessError('Called wait() on a stopped child ' + + 'process. This is not supported. Is some other ' + + 'process attempting job control with our child pid?') + return self.exitstatus + + def isalive(self): + '''This tests if the child process is running or not. This is + non-blocking. If the child was terminated then this will read the + exitstatus or signalstatus of the child. This returns True if the child + process appears to be running or False if not. It can take literally + SECONDS for Solaris to return the right status. ''' + + if self.terminated: + return False + + if self.flag_eof: + # This is for Linux, which requires the blocking form + # of waitpid to get the status of a defunct process. + # This is super-lame. The flag_eof would have been set + # in read_nonblocking(), so this should be safe. + waitpid_options = 0 + else: + waitpid_options = os.WNOHANG + + try: + pid, status = os.waitpid(self.pid, waitpid_options) + except OSError as e: + # No child processes + if e.errno == errno.ECHILD: + raise PtyProcessError('isalive() encountered condition ' + + 'where "terminated" is 0, but there was no child ' + + 'process. Did someone else call waitpid() ' + + 'on our process?') + else: + raise + + # I have to do this twice for Solaris. + # I can't even believe that I figured this out... + # If waitpid() returns 0 it means that no child process + # wishes to report, and the value of status is undefined. + if pid == 0: + try: + ### os.WNOHANG) # Solaris! + pid, status = os.waitpid(self.pid, waitpid_options) + except OSError as e: # pragma: no cover + # This should never happen... + if e.errno == errno.ECHILD: + raise PtyProcessError('isalive() encountered condition ' + + 'that should never happen. There was no child ' + + 'process. Did someone else call waitpid() ' + + 'on our process?') + else: + raise + + # If pid is still 0 after two calls to waitpid() then the process + # really is alive. This seems to work on all platforms, except for + # Irix which seems to require a blocking call on waitpid or select, + # so I let read_nonblocking take care of this situation + # (unfortunately, this requires waiting through the timeout). + if pid == 0: + return True + + if pid == 0: + return True + + if os.WIFEXITED(status): + self.status = status + self.exitstatus = os.WEXITSTATUS(status) + self.signalstatus = None + self.terminated = True + elif os.WIFSIGNALED(status): + self.status = status + self.exitstatus = None + self.signalstatus = os.WTERMSIG(status) + self.terminated = True + elif os.WIFSTOPPED(status): + raise PtyProcessError('isalive() encountered condition ' + + 'where child process is stopped. This is not ' + + 'supported. Is some other process attempting ' + + 'job control with our child pid?') + return False + + def kill(self, sig): + """Send the given signal to the child application. + + In keeping with UNIX tradition it has a misleading name. It does not + necessarily kill the child unless you send the right signal. See the + :mod:`signal` module for constants representing signal numbers. + """ + + # Same as os.kill, but the pid is given for you. + if self.isalive(): + os.kill(self.pid, sig) + + def getwinsize(self): + """Return the window size of the pseudoterminal as a tuple (rows, cols). + """ + TIOCGWINSZ = getattr(termios, 'TIOCGWINSZ', 1074295912) + s = struct.pack('HHHH', 0, 0, 0, 0) + x = fcntl.ioctl(self.fd, TIOCGWINSZ, s) + return struct.unpack('HHHH', x)[0:2] + + def setwinsize(self, rows, cols): + """Set the terminal window size of the child tty. + + This will cause a SIGWINCH signal to be sent to the child. This does not + change the physical window size. It changes the size reported to + TTY-aware applications like vi or curses -- applications that respond to + the SIGWINCH signal. + """ + return _setwinsize(self.fd, rows, cols) + + +class PtyProcessUnicode(PtyProcess): + """Unicode wrapper around a process running in a pseudoterminal. + + This class exposes a similar interface to :class:`PtyProcess`, but its read + methods return unicode, and its :meth:`write` accepts unicode. + """ + if PY3: + string_type = str + else: + string_type = unicode # analysis:ignore + + def __init__(self, pid, fd, encoding='utf-8', codec_errors='strict'): + super(PtyProcessUnicode, self).__init__(pid, fd) + self.encoding = encoding + self.codec_errors = codec_errors + self.decoder = codecs.getincrementaldecoder(encoding)(errors=codec_errors) + + def read(self, size=1024): + """Read at most ``size`` bytes from the pty, return them as unicode. + + Can block if there is nothing to read. Raises :exc:`EOFError` if the + terminal was closed. + + The size argument still refers to bytes, not unicode code points. + """ + b = super(PtyProcessUnicode, self).read(size) + return self.decoder.decode(b, final=False) + + def readline(self): + """Read one line from the pseudoterminal, and return it as unicode. + + Can block if there is nothing to read. Raises :exc:`EOFError` if the + terminal was closed. + """ + b = super(PtyProcessUnicode, self).readline() + return self.decoder.decode(b, final=False) + + def write(self, s): + """Write the unicode string ``s`` to the pseudoterminal. + + Returns the number of bytes written. + """ + b = s.encode(self.encoding) + return super(PtyProcessUnicode, self).write(b) diff --git a/class/ptyprocess/util.py b/class/ptyprocess/util.py new file mode 100644 index 00000000..aadbd62c --- /dev/null +++ b/class/ptyprocess/util.py @@ -0,0 +1,71 @@ +try: + from shutil import which # Python >= 3.3 +except ImportError: + import os, sys + + # This is copied from Python 3.4.1 + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + if path is None: + path = os.environ.get("PATH", os.defpath) + if not path: + return None + path = path.split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if not os.curdir in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + # If it does match, only test that one, otherwise we have to try + # others. + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if not normdir in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None + + +class PtyProcessError(Exception): + """Generic error class for this package.""" diff --git a/class/public.py b/class/public.py index 96a8876c..adde42a5 100644 --- a/class/public.py +++ b/class/public.py @@ -452,7 +452,7 @@ def getMsg(key,args = ()): # 获取Web服务器 def GetWebServer(): - if os.path.exists('/www/server/apache/bin/apachectl'): + if os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): webserver = 'apache' elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): webserver = 'openlitespeed' @@ -464,31 +464,66 @@ def get_webserver(): return GetWebServer() def ServiceReload(): - # 重载Web服务配置 - if os.path.exists('/www/server/nginx/sbin/nginx'): + #重载Web服务配置 + if os.path.exists('{}/nginx/sbin/nginx'.format(get_setup_path())): result = ExecShell('/etc/init.d/nginx reload') if result[1].find('nginx.pid') != -1: ExecShell('pkill -9 nginx && sleep 1') ExecShell('/etc/init.d/nginx start') - elif os.path.exists('/www/server/apache/bin/apachectl'): + elif os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): result = ExecShell('/etc/init.d/httpd reload') else: result = ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart') return result + def serviceReload(): return ServiceReload() -def ExecShell(cmdstring, timeout=None, shell=True,cwd=None,env=None): + +def get_preexec_fn(run_user): + ''' + @name 获取指定执行用户预处理函数 + @author hwliang<2021-08-19> + @param run_user 运行用户 + @return 预处理函数 + ''' + import pwd + pid = pwd.getpwnam(run_user) + uid = pid.pw_uid + gid = pid.pw_gid + + def _exec_rn(): + os.setgid(gid) + os.setuid(uid) + return _exec_rn + + +def ExecShell(cmdstring, timeout=None, shell=True,cwd=None,env=None,user = None): + ''' + @name 执行命令 + @author hwliang<2021-08-19> + @param cmdstring 命令 [必传] + @param timeout 超时时间 + @param shell 是否通过shell运行 + @param cwd 进入的目录 + @param env 环境变量 + @param user 执行用户名 + @return 命令执行结果 + ''' a = '' e = '' import subprocess,tempfile - + preexec_fn = None + tmp_dir = '/dev/shm' + if user: + preexec_fn = get_preexec_fn(user) + tmp_dir = '/tmp' try: rx = md5(cmdstring) - succ_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_succ',prefix='btex_' + rx ,dir='/dev/shm') - err_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_err',prefix='btex_' + rx ,dir='/dev/shm') - sub = subprocess.Popen(cmdstring, close_fds=True, shell=shell,bufsize=128,stdout=succ_f,stderr=err_f,cwd=cwd,env=env) + succ_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_succ',prefix='btex_' + rx ,dir=tmp_dir) + err_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_err',prefix='btex_' + rx ,dir=tmp_dir) + sub = subprocess.Popen(cmdstring, close_fds=True, shell=shell,bufsize=128,stdout=succ_f,stderr=err_f,cwd=cwd,env=env,preexec_fn=preexec_fn) if timeout: s = 0 d = 0.01 @@ -594,9 +629,9 @@ def get_client_ip(): return GetClientIp() def phpReload(version): - # 重载PHP配置 + #重载PHP配置 import os - if os.path.exists('/www/server/php/' + version + '/libphp5.so'): + if os.path.exists(get_setup_path()+'/php/' + version + '/libphp5.so'): ExecShell('/etc/init.d/httpd reload') else: ExecShell('/etc/init.d/php-fpm-'+version+' reload') @@ -893,12 +928,13 @@ def get_plugin_title(plugin_name): @return string ''' - info_file = '/www/server/panel/plugin/{}/info.json'.format(plugin_name) + info_file = '{}/{}/info.json'.format(get_plugin_path(),plugin_name) try: return json.loads(readFile(info_file))['title'] except: return plugin_name + def get_error_object(plugin_title = None,plugin_name = None): ''' @name 获取格式化错误响应对像 @@ -931,11 +967,11 @@ REQUEST_FORM: {request_form} full_path = request.full_path, request_form = request.form.to_dict(), user_agent = request.headers.get('User-Agent'), - panel_version = get_panel_version(), + panel_version = version(), os_version = get_os_version() ) - result =readFile('/www/server/panel/BTPanel/templates/default/plugin_error.html').format( + result =readFile('{}/BTPanel/templates/default/plugin_error.html'.format(get_panel_path())).format( plugin_name=plugin_title, request_info=request_info, error_title=error_info.split("\n")[-1], @@ -959,8 +995,9 @@ def format_date(format="%Y-%m-%d %H:%M:%S",times = None): # 检查Web服务器配置文件是否有错误 def checkWebConfig(): - f1 = '/www/server/panel/vhost/' - f2 = '/www/server/panel/plugin/' + f1 = '{}/'.format(get_vhost_path()) + f2 = '{}/'.format(get_plugin_path()) + setup_path = get_setup_path() if not os.path.exists(f2 + 'btwaf'): f3 = f1 + 'nginx/btwaf.conf' if os.path.exists(f3): os.remove(f3) @@ -974,19 +1011,19 @@ def checkWebConfig(): f3 = f1 + 'nginx/total.conf' if os.path.exists(f3): os.remove(f3) else: - if os.path.exists('/www/server/apache/modules/mod_lua.so'): - writeFile(f1 + 'apache/btwaf.conf', 'LoadModule lua_module modules/mod_lua.so') - writeFile(f1 + 'apache/total.conf', 'LuaHookLog /www/server/total/httpd_log.lua run_logs') + if os.path.exists(setup_path + '/apache/modules/mod_lua.so'): + writeFile(f1 + 'apache/btwaf.conf','LoadModule lua_module modules/mod_lua.so') + writeFile(f1 + 'apache/total.conf','LuaHookLog {}/total/httpd_log.lua run_logs'.format(setup_path)) else: f3 = f1 + 'apache/total.conf' if os.path.exists(f3): os.remove(f3) if get_webserver() == 'nginx': - result = ExecShell("ulimit -n 8192 ; /www/server/nginx/sbin/nginx -t -c /www/server/nginx/conf/nginx.conf") + result = ExecShell("ulimit -n 8192 ; {setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf".format(setup_path = setup_path)) searchStr = 'successful' elif get_webserver() == 'apache': # else: - result = ExecShell("ulimit -n 8192 ; /www/server/apache/bin/apachectl -t") + result = ExecShell("ulimit -n 8192 ; {setup_path}/apache/bin/apachectl -t".format(setup_path = setup_path)) searchStr = 'Syntax OK' else: result = ["1","1"] @@ -1060,7 +1097,7 @@ def CheckMyCnf(): if os.path.exists(confFile): conf = readFile(confFile) if conf.find('[mysqld]') != -1: return True - versionFile = '/www/server/mysql/version.pl' + versionFile = get_setup_path() + '/mysql/version.pl' if not os.path.exists(versionFile): return False versions = ['5.1','5.5','5.6','5.7','8.0','AliSQL'] @@ -1464,17 +1501,17 @@ def get_page(count, p=1, rows=12, callback='', result='1,2,3,4,5,8'): # 取面板版本 def version(): try: - comm = ReadFile('/www/server/panel/class/common.py') + comm = ReadFile('{}/common.py'.format(get_class_path())) return re.search("g\.version\s*=\s*'(\d+\.\d+\.\d+)'",comm).groups()[0] except: return get_panel_version() def get_panel_version(): - comm = ReadFile('/www/server/panel/class/common.py') + comm = ReadFile('{}/common.py'.format(get_class_path())) s_key = 'g.version = ' s_len = len(s_key) s_leff = comm.find(s_key) + s_len - version = comm[s_leff:s_leff+6].strip().strip("'") + version = comm[s_leff:s_leff+10].strip().strip("'") return version @@ -1490,7 +1527,10 @@ def get_os_version(): else: version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() v_info = sys.version_info - version = "{} {}(Py{}.{}.{})".format(version,os.uname().machine,v_info.major,v_info.minor,v_info.micro) + try: + version = "{} {}(Py{}.{}.{})".format(version,os.uname().machine,v_info.major,v_info.minor,v_info.micro) + except: + version = "{} (Py{}.{}.{})".format(version,v_info.major,v_info.minor,v_info.micro) return version #取文件或目录大小 @@ -1560,7 +1600,7 @@ def write_request_log(reques = None): if request.path in ['/service_status','/favicon.ico','/task','/system','/ajax','/control','/data','/ssl']: return False - log_path = '/www/server/panel/logs/request' + log_path = '{}/logs/request'.format(get_panel_path()) log_file = getDate(format='%Y-%m-%d') + '.json' if not os.path.exists(log_path): os.makedirs(log_path) @@ -1646,6 +1686,32 @@ def get_database_character(db_name): except: return 'utf8' +def get_database_codestr(codeing): + wheres = { + 'utf8' : 'utf8_general_ci', + 'utf8mb4' : 'utf8mb4_general_ci', + 'gbk' : 'gbk_chinese_ci', + 'big5' : 'big5_chinese_ci' + } + return wheres[codeing] + + +def get_database_size(): + """ + @获取数据库大小 + """ + data = {} + try: + import panelMysql + tables = panelMysql.panelMysql().query("select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES group by table_schema") + if type(tables) == list: + for x in tables: + if len(x) < 2:continue + if x[1] == None:continue + data[x[0]] = int(x[1]) + except: return data + return data + def en_punycode(domain): if sys.version_info[0] == 2: domain = domain.encode('utf8') @@ -1781,17 +1847,17 @@ def check_domain_panel(): #是否离线模式 def is_local(): - s_file = '/www/server/panel/data/not_network.pl' + s_file = '{}/data/not_network.pl'.format(get_panel_path()) return os.path.exists(s_file) #自动备份面板数据 def auto_backup_panel(): try: - panel_paeh = '/www/server/panel' + panel_paeh = get_panel_path() paths = panel_paeh + '/data/not_auto_backup.pl' if os.path.exists(paths): return False - b_path = '/www/backup/panel' + b_path = '{}/panel'.format(get_backup_path()) backup_path = b_path + '/' + format_date('%Y-%m-%d') if os.path.exists(backup_path): return True if os.path.getsize(panel_paeh + '/data/default.db') > 104857600 * 2: return False @@ -1946,7 +2012,7 @@ def get_fpm_address(php_version,bind=False): @return tuple or string ''' fpm_address = '/tmp/php-cgi-{}.sock'.format(php_version) - php_fpm_file = '/www/server/php/{}/etc/php-fpm.conf'.format(php_version) + php_fpm_file = '{}/php/{}/etc/php-fpm.conf'.format(get_setup_path(),php_version) try: fpm_conf = readFile(php_fpm_file) tmp = re.findall(r"listen\s*=\s*(.+)",fpm_conf) @@ -2020,9 +2086,10 @@ def get_site_php_version(siteName): @return string ''' web_server = get_webserver() - conf = readFile('/www/server/panel/vhost/'+web_server+'/'+siteName+'.conf') + vhost_path = get_vhost_path() + conf = readFile(vhost_path + '/'+web_server+'/'+siteName+'.conf') if web_server == 'openlitespeed': - conf = readFile('/www/server/panel/vhost/' + web_server + '/detail/' + siteName + '.conf') + conf = readFile(vhost_path + '/' + web_server + '/detail/' + siteName + '.conf') return get_php_version_conf(conf) @@ -2087,7 +2154,7 @@ def sync_php_address(php_version): @param php_version string PHP版本 @return void ''' - if not os.path.exists('/www/server/php/{}/bin/php'.format(php_version)): # 指定PHP版本是否安装 + if not os.path.exists('{}/php/{}/bin/php'.format(get_setup_path(),php_version)): # 指定PHP版本是否安装 return False ngx_rep = r"(unix:/tmp/php-cgi.*\.sock|127.0.0.1:\d+)" apa_rep = r"(unix:/tmp/php-cgi.*\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" @@ -2096,7 +2163,7 @@ def sync_php_address(php_version): is_write = False #nginx的PHP配置文件 - nginx_conf_path = '/www/server/nginx/conf' + nginx_conf_path = '{}/nginx/conf'.format(get_setup_path()) if os.path.exists(nginx_conf_path): for f_name in os.listdir(nginx_conf_path): @@ -2111,14 +2178,14 @@ def sync_php_address(php_version): # is_write = True #apache的网站配置文件 - apache_conf_path = '/www/server/panel/vhost/apache' + apache_conf_path = '{}/apache'.format(get_vhost_path()) if os.path.exists(apache_conf_path): for f_name in os.listdir(apache_conf_path): conf_file = '/'.join((apache_conf_path,f_name)) if sub_php_address(conf_file,apa_rep,apa_proxy,php_version): is_write = True #apache的phpmyadmin - conf_file = '/www/server/apache/conf/extra/httpd-vhosts.conf' + conf_file = '{}/apache/conf/extra/httpd-vhosts.conf'.format(get_setup_path()) if os.path.exists(conf_file): if sub_php_address(conf_file,apa_rep,apa_proxy,php_version): is_write = True @@ -2209,11 +2276,17 @@ def set_cdn_url(cdn_url): return True def get_python_bin(): - bin_file = '/www/server/panel/pyenv/bin/python' + bin_file = '{}/pyenv/bin/python'.format(get_panel_path()) if os.path.exists(bin_file): return bin_file return '/usr/bin/python' +def get_pip_bin(): + bin_file = '{}/pyenv/bin/pip'.format(get_panel_path()) + if os.path.exists(bin_file): + return bin_file + return '/usr/bin/pip' + def aes_encrypt(data,key): import panelAes if sys.version_info[0] == 2: @@ -2314,11 +2387,11 @@ def get_session_id(): #尝试自动恢复面板数据库 def rep_default_db(): - db_path = '/www/server/panel/data/' + db_path = '{}/data/'.format(get_panel_path()) db_file = db_path + 'default.db' db_tmp_backup = db_path + 'default_' + format_date("%Y%m%d_%H%M%S") + ".db" - panel_backup = '/www/backup/panel' + panel_backup = '{}/panel'.format(get_backup_path()) bak_list = os.listdir(panel_backup) if not bak_list: return False bak_list = sorted(bak_list,reverse=True) @@ -2362,7 +2435,7 @@ def chdck_salt(): def get_login_token(): - token_s = readFile('/www/server/panel/data/login_token.pl') + token_s = readFile('{}/data/login_token.pl'.format(get_panel_path())) if not token_s: return GetRandomString(32) return token_s @@ -2528,14 +2601,15 @@ def get_curl_bin(): #设置防跨站配置 def set_open_basedir(): try: - fastcgi_file = '/www/server/nginx/conf/fastcgi.conf' + fastcgi_file = '{}/nginx/conf/fastcgi.conf'.format(get_setup_path()) + if os.path.exists(fastcgi_file): fastcgi_body = readFile(fastcgi_file) if fastcgi_body.find('bt_safe_dir') == -1: fastcgi_body = fastcgi_body + "\n"+'fastcgi_param PHP_ADMIN_VALUE "$bt_safe_dir=$bt_safe_open";' writeFile(fastcgi_file,fastcgi_body) - proxy_file = '/www/server/nginx/conf/proxy.conf' + proxy_file = '{}/nginx/conf/proxy.conf'.format(get_setup_path()) if os.path.exists(proxy_file): proxy_body = readFile(proxy_file) if proxy_body.find('bt_safe_dir') == -1: @@ -2547,7 +2621,7 @@ map "baota_open" $bt_safe_open { } ''' writeFile(proxy_file,proxy_body) - open_basedir_path = '/www/server/panel/vhost/open_basedir/nginx' + open_basedir_path = '{}/open_basedir/nginx'.format(get_vhost_path()) if not os.path.exists(open_basedir_path): os.makedirs(open_basedir_path,384) @@ -2623,7 +2697,7 @@ def cloud_check_domain(domain): @return void ''' try: - check_domain_path = '/www/server/panel/data/check_domain/' + check_domain_path = '{}/data/check_domain/'.format(get_panel_path()) if not os.path.exists(check_domain_path): os.makedirs(check_domain_path,384) pdata = get_user_info() @@ -2634,9 +2708,13 @@ def cloud_check_domain(domain): except: pass +def get_mac_address(): + import uuid + mac=uuid.UUID(int = uuid.getnode()).hex[-12:] + return ":".join([mac[e:e+2] for e in range(0,11,2)]) def get_user_info(): - user_file = '/www/server/panel/data/userInfo.json' + user_file = '{}/data/userInfo.json'.format(get_panel_path()) if not os.path.exists(user_file): return {} userInfo = {} try: @@ -2866,6 +2944,11 @@ class dict_obj: elif format in ['phone','mobile','m']: if not re.match("^[0-9]{11,11}$",result): raise ValueError("Parameters: {}, mobile phone number format required".format(key)) + elif format in ['port']: + result_port = int(result) + if result_port > 65535 or result_port < 0: + raise ValueError("Parameters: {}, the required port number is 0-65535".format(key)) + result = result_port elif re.match(r"^[<>=]\d+$",result): operator = format[0] length = int(format[1:].strip()) @@ -2889,13 +2972,6 @@ class dict_obj: raise ValueError("The specified parameter value range is incorrect, {}:{}".format(key,limit)) return result - - - - - - - #实例化定目录下的所有模块 class get_modules: @@ -3124,7 +3200,7 @@ def login_send_body(is_type,username,login_ip,port): send_mail("aapanel login reminder","aapanel login reminder:Your server "+get_ip()+" successfully logged in via "+is_type+", account number: "+username+", login IP: "+login_ip+":"+port+", login time: "+time.strftime('%Y -%m-%d %X',time.localtime()), True) if os.path.exists(login_send_dingding): if check_ip_white(send_login_white,login_ip):return False - send_dingding("aapanel login reminder:Your server "+get_ip()+" successfully logged in via "+is_type+", account number: "+username+", login IP: "+login_ip+":"+port+", login time: "+time.strftime('%Y -%m-%d %X',time.localtime()), True) + send_dingding("aapanel login reminder:Your server "+get_ip()+" successfully logged in via "+is_type+", account number: "+username+", login IP: "+login_ip+":"+port+", login time: "+time.strftime('%Y -%m-%d %X',time.localtime())+'\n\n > Login status: Succeed', True) #普通模式下调用发送消息【设置登陆告警后的设置】 #title= 发送的title @@ -3211,6 +3287,15 @@ class PanelError(Exception): def __str__(self): return ("面板运行时发生错误: {}".format(repr(self.value))) +def get_sysbit(): + ''' + @name 获取操作系统位数 + @author hwliang<2021-07-07> + @return int 32 or 64 + ''' + import struct + return struct.calcsize('P') * 8 + def get_setup_path(): ''' @name 获取安装路径 @@ -3227,6 +3312,241 @@ def get_panel_path(): ''' return '{}/panel'.format(get_setup_path()) +def get_plugin_path(plugin_name = None): + ''' + @name 取指定插件目录 + @author hwliang<2021-07-14> + @param plugin_name 插件名称 不传则返回插件根目录 + @return string + ''' + + root_path = "{}/plugin".format(get_panel_path()) + if not plugin_name: return root_path + return "{}/{}".format(root_path,plugin_name) + +def get_class_path(): + ''' + @name 取类库所在路径 + @author hwliang<2021-07-14> + @return string + ''' + return "{}/class".format(get_panel_path()) + +def get_logs_path(): + ''' + @name 取日志目录 + @author hwliang<2021-07-14> + @return string + ''' + return '/www/wwwlogs' + +def get_vhost_path(): + ''' + @name 取虚拟主机目录 + @author hwliang<2021-08-14> + @return string + ''' + return '{}/vhost'.format(get_panel_path()) + + +def get_backup_path(): + ''' + @name 取备份目录 + @author hwliang<2021-07-14> + @return string + ''' + default_backup_path = '/www/backup' + backup_path = M('config').where("id=?",(1,)).getField('backup_path') + if not backup_path: return default_backup_path + if os.path.exists(backup_path): return backup_path + return default_backup_path + +def get_site_path(): + ''' + @name 取站点默认存储目录 + @author hwliang<2021-07-14> + @return string + ''' + default_site_path = '/www/wwwroot' + site_path = M('config').where("id=?",(1,)).getField('sites_path') + if not site_path: return default_site_path + if os.path.exists(site_path): return site_path + return default_site_path + + +def read_config(config_name,ext_name = 'json'): + ''' + @name 读取指定配置文件 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param ext_name 配置文件扩展名,默认为json + @return string 如果发生错误,将抛出PanelError异常 + ''' + config_file = "{}/config/{}.{}".format(get_panel_path(),config_name,ext_name) + if not os.path.exists(config_file): + raise PanelError('指定配置文件{} 不存在'.format(config_name)) + + config_str = readFile(config_file) + if ext_name == 'json': + try: + config_body = json.loads(config_str) + except Exception as ex: + raise PanelError('配置文件不是标准的可解析JSON内容!\n{}'.format(ex)) + return config_body + return config_str + +def save_config(config_name,config_body,ext_name = 'json'): + ''' + @name 保存配置文件 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param config_body 被保存的内容, ext_name为json,请传入可解析为json的参数类型,如list,dict,int,str等 + @param ext_name 配置文件扩展名,默认为json + @return string 如果发生错误,将抛出PanelError异常 + ''' + + config_file = "{}/config/{}.{}".format(get_panel_path(),config_name,ext_name) + if ext_name == 'json': + try: + config_body = json.dumps(config_body) + except Exception as ex: + raise PanelError('配置内容无法被转换为json格式!\n{}'.format(ex)) + + return writeFile(config_file,config_body) + +def get_config_value(config_name,key,default='',ext_name='json'): + ''' + @name 获取指定配置文件的指定配置项 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param key 配置项 + @param default 获不存在则返回的默认值,默认为空字符串 + @param ext_name 配置文件扩展名,默认为json + @return mixed 如果发生错误,将抛出PanelError异常 + ''' + config_data = read_config(config_name,ext_name) + return config_data.get(key,default) + +def set_config_value(config_name,key,value,ext_name='json'): + ''' + @name 设置指定配置文件的指定配置项 + @author hwliang<2021-07-14> + @param config_name 配置文件名称(不含扩展名) + @param key 配置项 + @param value 配置值 + @param ext_name 配置文件扩展名,默认为json + @return mixed 如果发生错误,将抛出PanelError异常 + ''' + config_data = read_config(config_name,ext_name) + config_data[key] = value + return save_config(config_name,config_data,ext_name) + + +def return_data(status,data = {},status_code=None,error_msg = None): + ''' + @name 格式化响应内容 + @author hwliang<2021-07-14> + @param status 状态 + @param data 响应数据 + @param status_code 状态码 + @param error_msg 错误消息内容 + @return dict + + ''' + if status_code is None: + status_code = 1 if status else 0 + if error_msg is None: + error_msg = '' if status else '未知错误' + + result = { + 'status':status, + "status_code":status_code, + 'error_msg':str(error_msg), + 'data':data + } + return result + + +def return_error(error_msg,status_code = -1,data = []): + ''' + @name 格式化错误响应内容 + @author hwliang<2021-07-15> + @param error_msg 错误消息 + @param status_code 状态码,默认为-1 + @param data 响应数据 + @return dict + ''' + return return_data(False,data,status_code,str(error_msg)) + + +def error(error_msg,status_code = -1,data = []): + ''' + @name 格式化错误响应内容 + @author hwliang<2021-07-15> + @param error_msg 错误消息 + @param status_code 状态码,默认为-1 + @param data 响应数据 + @return dict + ''' + return return_error(error_msg,status_code,data) + +def success(data = [],status_code = 1,error_msg = ''): + ''' + @name 格式化成功响应内容 + @author hwliang<2021-07-15> + @param data 响应数据 + @param status_code 状态码,默认为0 + @return dict + ''' + return return_data(True,data,status_code,error_msg) + + +def return_status_code(status_code,format_body,data = []): + ''' + @name 按状态码返回 + @author hwliang<2021-07-15> + @param status_code 状态码 + @param format_body 错误内容 + @param data 响应数据 + @return dict + ''' + error_msg = get_config_value('status_code',str(status_code)) + if not error_msg: raise PanelError('指定状态码不存在!') + return return_data(error_msg[0],data,status_code,error_msg[1].format(format_body)) + + +def to_dict_obj(data): + ''' + @name 将dict转换为dict_obj + @author hwliang<2021-07-15> + @param data 要被转换的数据 + @return dict_obj + ''' + if not isinstance(data,dict): + raise PanelError('错误的数据类型,只支持将dict转换为dict_obj') + pdata = dict_obj() + for key in data.keys(): + pdata[key] = data[key] + return pdata + +def get_script_object(filename): + ''' + @name 从脚本文件获取对像 + @author hwliang<2021-07-19> + @param filename 文件名 + @return object + ''' + _obj = sys.modules.get(filename,None) + if _obj: return _obj + from types import ModuleType + _obj = sys.modules.setdefault(filename, ModuleType(filename)) + _code = readFile(filename) + _code_object = compile(_code,filename, 'exec') + _obj.__file__ = filename + _obj.__package__ = '' + exec(_code_object, _obj.__dict__) + return _obj + def check_hooks(): ''' @name 自动注册HOOK @@ -3291,6 +3611,21 @@ def get_hook_index(mod_name, def_name): end_index = '{}_{}_END'.format(mod_name, def_name) return last_index, end_index + +def flush_plugin_list(): + ''' + @name 刷新插件列表 + @author hwliang<2021-07-22> + @return bool + ''' + skey = 'TNaMJdG3mDHKRS6Y' + from BTPanel import cache + from pluginAuth import Plugin + if cache.get(skey): cache.delete(skey) + Plugin(False).get_plugin_list(True) + return True + + def get_session_timeout(): ''' @name 获取session过期时间 @@ -3306,7 +3641,10 @@ def get_session_timeout(): session_timeout = 86400 if not os.path.exists(sess_out_path): return session_timeout - session_timeout = int(readFile(sess_out_path)) + try: + session_timeout = int(readFile(sess_out_path)) + except: + session_timeout = 86400 cache.set(skey,session_timeout,3600) return session_timeout @@ -3328,4 +3666,101 @@ def get_login_token_auth(): return login_token login_token = readFile(login_token_file) cache.set(skey,login_token,3600) - return login_token \ No newline at end of file + return login_token + + +def listen_ipv6(): + ''' + @name 是否监听ipv6 + @author hwliang<2021-08-12> + @return bool + ''' + ipv6_file = '{}/data/ipv6.pl'.format(get_panel_path()) + return os.path.exists(ipv6_file) + +def get_panel_log_file(): + ''' + @name 获取panel日志文件 + @author hwliang<2021-08-12> + @return string + ''' + return "{}/logs/error.log".format(get_panel_path()) + + +def print_log(_info,_level = 'DEBUG'): + ''' + @name 写入日志 + @author hwliang<2021-08-12> + @param _info 要写入到日志文件的信息 + @param _level 日志级别 + @return void + ''' + log_body = "[{}][{}] - {}\n".format(format_date(),_level.upper(),_info) + return WriteFile(get_panel_log_file(),log_body,'a+') + + + +def to_date(format = "%Y-%m-%d %H:%M:%S",times = None): + ''' + @name 格式时间转时间戳 + @author hwliang<2021-08-17> + @param format 时间格式 + @param times 时间 + @return int + ''' + ts = time.strptime(times, "%Y-%m-%d %H:%M:%S") + return time.mktime(ts) + + +def get_glibc_version(): + ''' + @name 获取glibc版本 + @author hwliang<2021-08-17> + @return string + ''' + try: + cmd_result = ExecShell("ldd --version")[0] + if not cmd_result: return '' + glibc_version = cmd_result.split("\n")[0].split()[-1] + except: + return '' + return glibc_version + + +def is_apache_nginx(): + ''' + @name 是否是apache或nginx + @author hwliang<2021-08-17> + @return bool + ''' + setup_path = get_setup_path() + return os.path.exists(setup_path + '/apache') or os.path.exists(setup_path + '/nginx') + +def error_403(e): + from BTPanel import Response + errorStr = ''' +403 Forbidden + +

          403 Forbidden

          +
          nginx
          + +''' + headers = { + "Content-Type": "text/html" + } + return Response(errorStr, status=403, headers=headers) + +def error_404(e): + from BTPanel import Response + errorStr = ''' +404 Not Found + +

          404 Not Found

          +
          nginx
          + +''' + headers = { + "Content-Type": "text/html" + } + return Response(errorStr, status=404, headers=headers) + diff --git a/class/send_mail.py b/class/send_mail.py index 8b3247cf..84d7ede9 100644 --- a/class/send_mail.py +++ b/class/send_mail.py @@ -131,9 +131,9 @@ class send_mail: msg['To']=formataddr(email) msg['Subject'] = title if int(self.__qq_mail_user['port']) == 465: - server = smtplib.SMTP_SSL(str(self.__qq_mail_user['hosts']), str(self.__qq_mail_user['port'])) + server = smtplib.SMTP_SSL(str(self.__qq_mail_user['hosts']), str(self.__qq_mail_user['port']),timeout=5) else: - server = smtplib.SMTP(str(self.__qq_mail_user['hosts']), str(self.__qq_mail_user['port'])) + server = smtplib.SMTP(str(self.__qq_mail_user['hosts']), str(self.__qq_mail_user['port']),timeout=5) server.login(self.__qq_mail_user['qq_mail'], self.__qq_mail_user['qq_stmp_pwd']) if type(email)==str: server.sendmail(self.__qq_mail_user['qq_mail'], [email.strip()], msg.as_string()) @@ -141,6 +141,7 @@ class send_mail: server.sendmail(self.__qq_mail_user['qq_mail'], email, msg.as_string()) server.quit() except Exception: + print("发送错误,可能密码错误") ret = False return ret @@ -206,18 +207,27 @@ class send_mail: # 钉钉机器人 def dingding_send(self, content): if 'dingding_url' not in self.__dingding_info or 'isAtAll' not in self.__dingding_info or 'user' not in self.__dingding_info: return -1 - data = { - "msgtype": "text", - "text": { - "content": content - }, - "at": { - "atMobiles": [ - self.__dingding_info['user'] - ], - "isAtAll": self.__dingding_info['isAtAll'] + if 'weixin.qq.com' in self.__dingding_info['dingding_url']: + data = { + "msgtype": "markdown", + "markdown": { + "content": content + } + } + else: + data = { + "msgtype": "markdown", + "markdown": { + "title": "Server notification", + "text": content + }, + "at": { + "atMobiles": [ + self.__dingding_info['user'] + ], + "isAtAll": self.__dingding_info['isAtAll'] + } } - } headers = {'Content-Type': 'application/json'} try: x = requests.post(url=self.__dingding_info['dingding_url'], data=json.dumps(data), headers=headers, diff --git a/class/setPanelLets.py b/class/setPanelLets.py index ab653393..718182e6 100644 --- a/class/setPanelLets.py +++ b/class/setPanelLets.py @@ -84,12 +84,22 @@ class setPanelLets: pssl = panelSSL.panelSSL() gcl = pssl.GetCertList(get) for i in gcl: - for v in i.values(): - if get.domain == v: + if get.domain in i['dns'] or get.domain == i['subject']: + try: + time_stamp = int(i['notAfter']) + except: + time_array = time.strptime(i['notAfter'],"%Y-%m-%d") + time_stamp = int(time.mktime(time_array)) + now = time.time() + if time_stamp > int(now): + return i + for d in i['dns']: + d = d.split('.') + if '*' in d and d[1:] == get.domain.split('.')[1:]: try: time_stamp = int(i['notAfter']) except: - time_array = time.strptime(i['notAfter'],"%Y-%m-%d") + time_array = time.strptime(i['notAfter'], "%Y-%m-%d") time_stamp = int(time.mktime(time_array)) now = time.time() if time_stamp > int(now): diff --git a/class/ssh_authentication.py b/class/ssh_authentication.py new file mode 100644 index 00000000..4c0c5665 --- /dev/null +++ b/class/ssh_authentication.py @@ -0,0 +1,337 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | version :1.0 +# +------------------------------------------------------------------- +# | Author: 梁凯强 <1249648969@qq.com> +# +------------------------------------------------------------------- +# | SSH 双因子认证 +# +-------------------------------------------------------------------- +import public,re,os +import platform,time + +class ssh_authentication: + __SSH_CONFIG='/etc/ssh/sshd_config' + __PAM_CONFIG='/etc/pam.d/sshd' + __python_pam='/usr/pam_python_so' + __config_pl='/www/server/panel/data/pam_btssh_authentication.pl' + + + def __init__(self): + '''检查pam_python目录是否存在''' + if not os.path.exists(self.__python_pam): + public.ExecShell("mkdir -p " + self.__python_pam) + public.ExecShell("chmod 600 " + self.__python_pam) + if not os.path.exists(self.__config_pl): + public.ExecShell("echo '%s' >>%s"%(public.GetRandomString(32),self.__config_pl)) + public.ExecShell("chmod 600 " + self.__config_pl) + + def wirte(self, file, ret): + result = public.writeFile(file, ret) + return result + + #重启SSH + def restart_ssh(self): + act = 'restart' + if os.path.exists('/etc/redhat-release'): + version = public.readFile('/etc/redhat-release') + if isinstance(version, str): + if version.find(' 7.') != -1 or version.find(' 8.') != -1: + public.ExecShell("systemctl " + act + " sshd.service") + else: + public.ExecShell("/etc/init.d/sshd " + act) + else: + public.ExecShell("/etc/init.d/sshd " + act) + else: + public.ExecShell("/etc/init.d/sshd " + act) + + #查找PAM目录 + def get_pam_dir(self): + #Centos 系列 + if os.path.exists('/etc/redhat-release'): + version = public.readFile('/etc/redhat-release') + if isinstance(version, str): + if version.find(' 7.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find(' 8.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + else: + return False + #Ubuntu + elif os.path.exists('/etc/lsb-release'): + version = public.readFile('/etc/lsb-release') + if isinstance(version, str): + if version.find('16.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find('20.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find('18.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + else: + return False + #debian + elif os.path.exists('/etc/debian_version'): + version = public.readFile('/etc/debian_version') + if isinstance(version, str): + if version.find('9.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find('10.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + else: + return False + return False + + #判断PAMSO文件是否存在 + def isPamSoExists(self): + check2=self.get_pam_dir() + if not check2: return False + check=check2.split() + if len(check)<3: return False + if os.path.exists(check[2]): + #判断文件大小 + if os.path.getsize(check[2])<10240: + self.install_pam_python(check) + return self.isPamSoExists() + return check2 + else: + self.install_pam_python(check) + return self.isPamSoExists() + + #安装pam_python + def install_pam_python(self,check): + so_path=check[2] + so_name=check[2].split('/')[-1] + public.ExecShell('/usr/local/curl/bin/curl -o %s http://download.bt.cn/btwaf_rule/pam_python_so/%s'%(so_path,so_name)) + public.ExecShell("chmod 600 " + so_path) + return True + + #开启双因子认证 + def start_ssh_authentication(self): + check=self.isPamSoExists() + if not check:return False + if os.path.exists(self.__PAM_CONFIG): + auth_data=public.readFile(self.__PAM_CONFIG) + if isinstance(auth_data, str): + if auth_data.find("\n"+check) != -1: + return True + else: + auth_data=auth_data+"\n"+check + public.writeFile(self.__PAM_CONFIG,auth_data) + return True + return False + + #关闭双因子认证 + def stop_ssh_authentication(self): + check=self.isPamSoExists() + if not check:return False + if os.path.exists(self.__PAM_CONFIG): + auth_data=public.readFile(self.__PAM_CONFIG) + if isinstance(auth_data, str): + if auth_data.find("\n"+check) != -1: + auth_data=auth_data.replace("\n"+check,'') + public.writeFile(self.__PAM_CONFIG,auth_data) + return True + else: + return False + return False + + #检查是否开启双因子认证 + def check_ssh_authentication(self): + check=self.isPamSoExists() + if not check:return False + if os.path.exists(self.__PAM_CONFIG): + auth_data=public.readFile(self.__PAM_CONFIG) + if isinstance(auth_data, str): + if auth_data.find("\n"+check) != -1: + return True + else: + return False + return False + + + #设置SSH应答模式 + def set_ssh_login_user(self): + ssh_password = '\nChallengeResponseAuthentication\s\w+' + file = public.readFile(self.__SSH_CONFIG) + if isinstance(file, str): + if len(re.findall(ssh_password, file)) == 0: + file_result = file + '\nChallengeResponseAuthentication yes' + else: + file_result = re.sub(ssh_password, '\nChallengeResponseAuthentication yes', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, 'Successfully opened') + + #关闭SSH应答模式 + def close_ssh_login_user(self): + file = public.readFile(self.__SSH_CONFIG) + ssh_password = '\nChallengeResponseAuthentication\s\w+' + if isinstance(file, str): + file_result = re.sub(ssh_password, '\nChallengeResponseAuthentication no', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, 'Closed successfully') + + #查看SSH应答模式 + def check_ssh_login_user(self): + file = public.readFile(self.__SSH_CONFIG) + ssh_password = '\nChallengeResponseAuthentication\s\w+' + if isinstance(file, str): + ret = re.findall(ssh_password, file) + if not ret: + return False + else: + if ret[-1].split()[-1] == 'yes': + return True + else: + return False + return False + + #关闭密码访问 + def stop_password(self): + ''' + 关闭密码访问 + 无参数传递 + ''' + file = public.readFile(self.__SSH_CONFIG) + if isinstance(file, str): + if file.find('PasswordAuthentication') != -1: + file_result = file.replace('\nPasswordAuthentication yes', '\nPasswordAuthentication no') + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, 'Closed successfully') + else: + return public.returnMsg(False, 'No password authentication') + return public.returnMsg(False, 'No password authentication') + + #开启密码登录 + def start_password(self): + ''' + 开启密码登陆 + get: 无需传递参数 + ''' + file = public.readFile(self.__SSH_CONFIG) + if isinstance(file, str): + if file.find('PasswordAuthentication') != -1: + file_result = file.replace('\nPasswordAuthentication no', '\nPasswordAuthentication yes') + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, 'Turn on password authentication successfully') + else: + file_result = file + '\nPasswordAuthentication yes' + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, 'Turn on password authentication successfully') + return public.returnMsg(False, 'No password authentication') + + #查看密码登录状态 + def check_password(self): + ''' + 查看密码登录状态 + 无参数传递 + ''' + file = public.readFile(self.__SSH_CONFIG) + ssh_password = '\nPasswordAuthentication\s\w+' + if isinstance(file, str): + ret = re.findall(ssh_password, file) + if not ret: + return False + else: + if ret[-1].split()[-1] == 'yes': + return True + else: + return False + return False + + + #开启SSH 双因子认证 + def start_ssh_authentication_two_factors(self): + if not self.get_pam_dir():return public.returnMsg(False,'The system is not supported') + check=self.isPamSoExists() + if not check:return 'False' + if not self.check_ssh_login_user(): + self.set_ssh_login_user() + if not self.check_ssh_authentication(): + self.start_ssh_authentication() + #如果开启的话,就关闭密码认证 + # if self.check_password(): + # self.stop_password() + #检查是否开启双因子认证 + if self.check_ssh_authentication() and self.check_ssh_login_user(): + return public.returnMsg(True,'Successfully opened') + return public.returnMsg(True,'Failed to open') + + + #关闭SSH 双因子认证 + def close_ssh_authentication_two_factors(self): + if not self.get_pam_dir():return public.returnMsg(False,'The system is not supported') + check=self.isPamSoExists() + if not check:return False + if self.check_ssh_authentication(): + self.stop_ssh_authentication() + #检查是否关闭双因子认证 + #如果是关闭的SSH,那么就开启 + # if not self.check_password(): + # self.start_password() + if not self.check_ssh_authentication(): + return public.returnMsg(True,'Closed') + if self.stop_ssh_authentication(): + return public.returnMsg(True,'Closed') + + + #检查是否开启双因子认证 + def check_ssh_authentication_two_factors(self): + if not self.get_pam_dir():return public.returnMsg(False,'The system is not supported') + check=self.isPamSoExists() + if not check:return False + if not self.check_ssh_login_user(): + return public.returnMsg(False,'Inactive') + if not self.check_ssh_authentication(): + return public.returnMsg(False,'Inactive') + return public.returnMsg(True,'Activated') + + def is_check_so(self): + '''判断SO文件是否存在''' + if not self.get_pam_dir():return public.returnMsg(False,'The system is not supported') + config_data=self.get_pam_dir() + if not config_data:return False + config_data2=config_data.split() + ret={} + ret['so_path']=config_data2[2].split('/')[-1] + if os.path.exists(config_data2[2]): + ret['so_status']=True + else: + ret['so_status']=False + return public.returnMsg(True,ret) + + def download_so(self): + '''下载so文件''' + if not self.get_pam_dir():return public.returnMsg(False,'The system is not supported') + config_data=self.get_pam_dir() + if not config_data:return False + config_data=config_data.split() + self.install_pam_python(config_data) + #判断下载的文件大小 + if os.path.exists(config_data[2]) : + if os.path.getsize(config_data[2])>10240: + return public.returnMsg(True,"Download the file successfully") + return public.returnMsg(False,"download failed") + + #获取Linux系统的主机名 + def get_pin(self): + import platform,time + data=platform.uname() + tme_data=time.strftime('%Y-%m-%d%H:%M',time.localtime(time.time())) + #获取秒 + tis_data=time.strftime('%S',time.localtime(time.time())) + ip_list=public.ReadFile('/www/server/panel/data/pam_btssh_authentication.pl') + ret={} + if isinstance(ip_list,str): + info=data[0]+data[1]+data[2]+tme_data+ip_list + md5_info=public.Md5(info) + ret['pin']=md5_info[:6] + ret['time']=60-int(tis_data) + return ret + else: + ret['pin']='error' + ret['time']=60 + return ret \ No newline at end of file diff --git a/class/userlogin.py b/class/userlogin.py index 12cd1c25..16dab766 100644 --- a/class/userlogin.py +++ b/class/userlogin.py @@ -46,7 +46,8 @@ class userlogin: return public.returnJson(False,'LOGIN_USER_ERR',(str(num),)),json_header _key_file = "/www/server/panel/data/two_step_auth.txt" #登陆告警 - public.login_send_body("Userinfo",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))) + public.run_thread(public.login_send_body,("Userinfo",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT')))) + # public.login_send_body("Userinfo",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))) if hasattr(post,'vcode'): if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'You have failed verification many times, forbidden for 10 minutes'),json_header import pyotp diff --git a/tools.py b/tools.py index d03349d8..a7485d1f 100644 --- a/tools.py +++ b/tools.py @@ -450,6 +450,53 @@ def bt_cli(u_input = 0): u_input = input(public.GetMsg("INPUT_CMD_NUM")) if sys.version_info[0] == 3: u_input = int(u_input) except: u_input = 0 + try: + if u_input in ['log','logs','error','err','tail','debug','info']: + os.system("tail -f {}".format(public.get_panel_log_file())) + return + if u_input[:6] in ['install','update']: + print("Tip: Example of command parameter transfer (compile and install php7.4):bt install/0/php/7.4") + print(sys.argv) + install_args = u_input.split('/') + if len(install_args) < 2: + try: + install_input = input("Please select the installation method (0 compile install, 1 speed install, default: 1):") + install_input = int(install_input) + except: + install_input = 1 + else: + install_input = int(install_args[1]) + print(raw_tip) + soft_list = 'nginx apache php mysql memcached redis pure-ftpd phpmyadmin pm2 docker openlitespeed mongodb' + soft_list_arr = soft_list.split(' ') + if len(install_args) < 3: + install_soft = '' + while not install_soft: + print("Supported software:{}".format(soft_list)) + print(raw_tip) + install_soft = input("Please enter the name of the software to be installed (eg: nginx):") + if install_soft not in soft_list_arr: + print("Software that does not support command line installation") + install_soft = '' + else: + install_soft = install_args[2] + + print(raw_tip) + if len(install_args) < 4: + install_version = '' + while not install_version: + print(raw_tip) + install_version = input("Please enter the version number to be installed (for example: 1.18):") + else: + install_version = install_args[3] + + print(raw_tip) + os.system("bash /www/server/panel/install/install_soft.sh {} {} {} {}".format(install_input,install_args[0],install_soft,install_version)) + exit() + + print("Unsupported command") + exit() + except: pass nums = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,22,23,24,25,26] if not u_input in nums: @@ -639,7 +686,11 @@ if __name__ == "__main__": update_to6() elif type == "cli": clinum = 0 - if len(sys.argv) > 2: clinum = int(sys.argv[2]) + try: + if len(sys.argv) > 2: + clinum = int(sys.argv[2]) if sys.argv[2][:6] not in ['instal','update'] else sys.argv[2] + except: + clinum = sys.argv[2] bt_cli(clinum) else: print('ERROR: Parameter error')