File manager - Edit - /usr/local/cpanel/whostmgr/docroot/templates/cpanel_customization/index.cmb.js
Back
/* # cpanel - whostmgr/docroot/templates/cpanel_customization/constants.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ define('app/constants',["cjt/util/locale"], function(LOCALE) { "use strict"; var DEFAULT_PRIMARY_DARK = "#08193E"; // $cp-midnight-express from /usr/local/cpanel/base/frontend/jupiter/base_styles/00_configuration/_cp_colors.scss var DEFAULT_PRIMARY_LIGHT = "#ffffff"; // $cp-white from /usr/local/cpanel/base/frontend/jupiter/base_styles/00_configuration/_cp_colors.scss return { // Image EMBEDDED_SVG: "data:image/svg+xml;base64,", EMBEDDED_ICO: "data:image/x-icon;base64,", DATA_URL_PREFIX_REGEX: /^data:[^,]*,/, // Colors - It would be nice to figure out a way to load these from the scss file, but I cant see a good way without DEFAULT_PRIMARY_DARK: DEFAULT_PRIMARY_DARK, DEFAULT_PRIMARY_LIGHT: DEFAULT_PRIMARY_LIGHT, DEFAULT_COLORS: { primary: DEFAULT_PRIMARY_DARK, }, // File upload sizes MAX_FILE_SIZE: 100 * 1000, // 100 kilobytes // Tabs GENERAL_TABS_INFO: { logos: LOCALE.maketext("Logos"), colors: LOCALE.maketext("Colors"), favicon: LOCALE.maketext("Favicon"), links: LOCALE.maketext("Links"), "public-contact": LOCALE.maketext("Public Contact"), }, JUPITER_TAB_ORDER: [ "logos", "colors", "favicon", "links", "public-contact", ], JUPITER_TAB_INDEX: { "logos": 3, "colors": 4, "favicon": 5, "links": 6, "public-contact": 10, }, // Routing DEFAULT_THEME: "jupiter", DEFAULT_ROUTE: "logos", }; }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/services/savedService.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ /* jshint -W089 */ /* jshint -W018 */ define( 'app/services/savedService',[ // Libraries "angular", ], function(angular, API, APIREQUEST) { "use strict"; // Fetch the current application var app = angular.module("customize.services.savedService", []); app.factory("savedService", [function() { var tabs = {}; var DEFAULT = { dirty: false, }; // return the factory interface return { /** * Register all the participating tabs * * @param {string[]} tabNames */ registerTabs: function(tabNames) { tabNames.forEach(function(tabName) { tabs[tabName] = angular.copy(DEFAULT); }); }, /** * Check if the tab or tabs need to be saved. If you provide a `tabName` it * only looks at the one tab, if not it checks all the registered tabs. * * NOTE: Not all tabs are registered currently. Unregisted tabs return false * since they are not yet participating in this system. * * @param {string} tabName * @returns {boolean} true if you need to save something, false otherwise. */ needToSave: function(tabName) { if (!tabName) { // Check all the tabs return Object.keys(tabs).some(function(tabName) { return tabs[tabName] ? tabs[tabName].dirty : false; }); } else { // Check just the requested tab return tabs[tabName] ? tabs[tabName].dirty : false; } }, /** * Mark a tab state: * * * dirty = true, need to save it. * * dirty = false, its saved already. * * @param {string} tabName * @param {boolean} dirty */ update: function(tabName, dirty) { if (tabs[tabName]) { tabs[tabName].dirty = dirty; } }, }; }]); }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/services/beforeUnloadService.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ define('app/services/beforeUnloadService',[ "angular", ], function(angular) { "use strict"; /* * This service is used to send events when the window * onbeforeunload and onunload event happen. * * The service will broadcast the following events: * * onBeforeUnload - application code can register for this event on the scope. This * event can be used to prevent the unload using the `event.preventDefault()` technique. * Note, not all browsers will show the unload popup. Some also wont show it unless the * user has interacted with the form. Some browser will not use the custom message. * * onUnload - application code can register for this event on the scope. It can be used * to clean up any resources or cancel outstanding remote calls. You can not cancel the * unload from this event. */ angular.module("customize.services.beforeUnloadService", []) .factory("beforeUnload", [ "$rootScope", "$window", function($rootScope, $window) { /** * Handler for the browser onbeforeunload event. * * @param {Event} e * @returns {string|undefined} - A message to show the user when deciding if they want to cancel the unload. */ $rootScope.doBeforeUnload = function(e) { var config = {}; /** * @typedef {Config} * @property {string} prompt - the propt to tell the user. */ var event = $rootScope.$broadcast("onBeforeUnload", config); if (event.defaultPrevented) { e.preventDefault(); // Note: Some browsers will not show this message, but instead have their own. e.returnValue = config.prompt || ""; // For some Chrome browsers return config.prompt; } else { delete e["returnValue"]; // For some Chrome browsers return false; } }; /** * Handler for the browser unload event */ $rootScope.doUnload = function() { $rootScope.$broadcast("onUnload"); }; $window.addEventListener("beforeunload", $rootScope.doBeforeUnload); $window.addEventListener("onunload", $rootScope.doUnload); return {}; } ] ) .run(["beforeUnload", function(beforeUnload) { // Must invoke the service at least once } ] ); }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/directives/fileReader.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ define( 'app/directives/fileReader',[ "angular", "cjt/util/locale", "app/constants", ], function(angular, LOCALE, CONSTANTS) { "use strict"; var module = angular.module("customize.directives.fileReader", []); module.directive("fileReader", [ "$q", function($q) { return { restrict: "A", require: [ "?ngModel" ], priority: 20, link: function(scope, element, attrs, controllers) { var ngModel = controllers[0]; // To use this attribute directive a parent element must: // * have an ngModel attribute // * be an <input> element // * have an type attribute of 'file' if (!ngModel || element[0].tagName !== "INPUT" || !attrs["type"] || attrs["type"] !== "file") { return; } ngModel.$render = function() {}; /** * Helper used to mock behavior in tests * @param {HtmlFileInput} el * @returns {File} */ scope._getFiles = function(el) { return el.files; }; /** * Helper used to mock behavior in tests * @returns {number} */ scope._getMaxSize = function() { return CONSTANTS.MAX_FILE_SIZE; }; /** * Merge file data to the model object. * * @param {ngModelController} ngModel * @param {File} file * @param {string} data * @returns {Object} The updated model. */ scope._mergeToModel = function(ngModel, file, data) { var model = angular.copy(ngModel.$modelValue); model.filename = file.name; model.size = file.size; model.type = file.type; model.data = data; delete model.error; delete model.errorKey; return model; }; /** * Update the model with the error information. * * @param {Object} model - Model to store the data into. * @param {String} message - Human readable message * @param {String} key - Machine identifier for the error. */ scope._setModelError = function(model, message, key) { model.error = message; model.errorKey = key; }; /** * Converts a file into a base64 encoded string. * * @async * @param {File} file - As defined here: https://developer.mozilla.org/en-US/docs/Web/API/File * @returns {string} The base64 encoded file contents. */ scope._toBase64 = function(file) { var deferred = $q.defer(); var maxSize = scope._getMaxSize(); var model; if (file.size > maxSize) { // We resolve instead of reject to allow the validation to get all // the info and make decisions based on it. We only exclude the costly // .data property model = scope._mergeToModel(ngModel, file, ""); scope._setModelError(model, LOCALE.maketext("The file is larger than the maximum of [numf,_1] kilobytes.", maxSize), "maxsize"); deferred.resolve(model); return deferred.promise; } var reader = new FileReader(); reader.addEventListener("loadend", function(e) { scope.$apply(function() { model = scope._mergeToModel(ngModel, file, e.target.result); deferred.resolve(model); }); }); reader.addEventListener("error", function(e) { scope.$apply(function() { // We resolve instead of reject to allow the validation to get all // the info and make decisions based on it. We only exclude the costly // .data property model = scope._mergeToModel(ngModel, file, ""); scope._setModelError(model, e && e.target ? e.target.error : LOCALE.maketext("An unknown error occurred while reading the file contents."), "readFailed"); deferred.resolve(model); }); }); try { reader.readAsDataURL(file); } catch (error) { // Catch any synchronous errors. // We resolve instead of reject to allow the validation to get all // the info and make decisions based on it. We only exclude the costly // .data property model = scope._mergeToModel(ngModel, file, ""); scope._setModelError(model, error ? error : LOCALE.maketext("An unknown error occurred while reading the file contents."), "readFailed"); deferred.resolve(model); } return deferred.promise; }; element.bind("change", function(e) { scope.$apply(function() { var el = e.target; var files = scope._getFiles(el); if (!files || !files.length) { return; } var file = files[0]; if (file && file.size !== 0) { scope._toBase64(file) .then(function(value) { ngModel.$setViewValue(value); // NOTE: This will trigger the $parsers above. }); } return; }); }); }, }; }, ]); } ); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/directives/fileType.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ define('app/directives/fileType',[ "angular", ], function(angular) { "use strict"; var module = angular.module("customize.directives.fileType", []); // This directive validates an <input type="file"> based on the "type" property of a selected file. // The file-type attribute should contain an expression defining an array of valid types. module.directive("fileType", [function() { function checkType(file, types) { return types.some(function(type) { return file.type === type; }); } return { restrict: "A", require: "ngModel", link: function link($scope, $element, $attrs, $ngModelCtrl) { $element.bind("change", function() { var file = this.files[0]; if (file && !checkType(file, $scope.$eval($attrs.fileType))) { $ngModelCtrl.$setValidity("filetype", false); } else { $ngModelCtrl.$setValidity("filetype", true); } }); }, }; }]); }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/directives/triggerFor.js # Copyright 2020 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ define('app/directives/triggerFor',[ "angular", ], function(angular) { "use strict"; var module = angular.module("customize.directives.triggerFor", []); // This directive will trigger a "click" event on another element when the linked element is clicked. module.directive("triggerFor", [function() { return { restrict: "A", link: function link($scope, $element, $attrs) { $element.bind("click", function() { document.querySelector("#" + $attrs.triggerFor).click(); }); }, }; }]); }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/directives/fileUpload.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ define( 'app/directives/fileUpload',[ "angular", "cjt/util/locale", "cjt/core", "uiBootstrap", "app/directives/fileReader", "app/directives/fileType", "app/directives/triggerFor", ], function(angular, LOCALE, CJT) { "use strict"; var module = angular.module("customize.directives.fileUpload", [ "customize.directives.fileReader", "customize.directives.fileType", "customize.directives.triggerFor", "ui.bootstrap", ]); module.directive("cpFileUpload", [ "$uibModal", function($uibModal) { var TEMPLATE_PATH = "directives/fileUpload.phtml"; var RELATIVE_PATH = "templates/cpanel_customization/" + TEMPLATE_PATH; return { replace: true, require: [ "^form", "ngModel" ], restrict: "E", scope: { id: "@id", title: "@title", browseButtonLabel: "@browseButtonLabel", deleteButtonTitle: "@deleteButtonTitle", confirmDeleteMessage: "@confirmDeleteMessage", previewTitle: "@previewTitle", help: "@help", fileTypeError: "@fileTypeError", fileEmptyError: "@fileEmptyError", fileMaxSizeError: "@fileMaxSizeError", model: "=ngModel", types: "=mimeTypes", previewBgColor: "=previewBgColor", onDelete: "&onDelete", onChange: "&onChange", onReset: "&onReset", inputClasses: "@inputClass", previewClasses: "@previewClass", }, templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH, link: function(scope, element, attrs, controllers) { scope.LOCALE = LOCALE; var formController = controllers[0]; var modelController = controllers[1]; if (!scope.inputClasses) { scope.inputClasses = "col-xs-12 col-sm-8 col-md-8 col-lg-6"; } if (!scope.previewClasses) { scope.previewClasses = "col-xs-12 col-sm-4 col-md-4 col-lg-4"; } /** * Generate a complete field name for a field in the upload form partial. * * @param {string} field - the partial name of the field in the form * @returns {string} */ scope.field = function(field) { return "file_upload_" + field; }; /** * Check if the given form field is pritine. * * @param {string} field - the partial name of the field in the form * @returns {boolean} - true if its pristine, false otherwise. */ scope.isPristine = function(field) { var fieldName = scope.field(field); var f = formController[fieldName]; if (f) { return f.$pristine; } return true; }; /** * Check if the given form field is invalid. * * @param {string} field - the partial name of the field in the form * @returns {boolean} - true if its invalid, false otherwise. */ scope.isInvalid = function(field) { var fieldName = scope.field(field); var f = formController[fieldName]; if (f) { return f.$invalid; } return true; }; /** * Checks if the given field has the given error. * * @param {string} field - the partial name of the field in the form * @param {string} kind - name of the error you are looking for. * @returns {boolean} - the error object if present, undefined otherwise. */ scope.errors = function(field, kind) { var fieldName = scope.field(field); var f = formController[fieldName]; if (f) { return f.$error[kind]; } return; }; /** * Set the field to pristine. * * @param {string} field */ scope.setPristine = function(field) { var fieldName = scope.field(field); var f = formController[fieldName]; if (f) { f.$setPristine(); } return; }; /** * Creates a clean copy of the model. * * @param {FileModel} model * @returns {FileModel} */ function cleanCopy(original) { var model = angular.copy(original); model.data = ""; model.filename = ""; model.size = 0; model.type = ""; delete model.error; delete model.errorType; return model; } /** * @typedef FileModel * @property {string} filename - name of the file to display * @property {string} data - base 64 encoded file contents * @property {boolean} saved - true if the file has been saved to the backend, false otherwise. * @property {string} name - name of the property */ /** * Initiate a delete file operation * @param {FileModel} model - the data model */ scope.delete = function(model) { scope.deleteLoading = true; var promise = scope.onDelete(model); if (promise && promise.then) { promise.then(function() { scope.deleteLoading = false; scope.model = cleanCopy(modelController.$modelValue); scope.setPristine(scope.id + "_file"); }); } else { scope.deleteLoading = false; scope.model = cleanCopy(modelController.$modelValue); scope.setPristine(scope.id + "_file"); } }; /** * Confirms deleting * * @method confirmDelete * @param {FileModel} model - the data model */ scope.confirmDelete = function(model) { if (model.saved) { var TEMPLATE_PATH = "directives/fileConfirmUploadDelete.phtml"; var RELATIVE_PATH = "templates/cpanel_customization/" + TEMPLATE_PATH; $uibModal.open({ templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH, size: "sm", controller: [ "$scope", "$uibModalInstance", function($scope, $uibModalInstance) { $scope.message = scope.confirmDeleteMessage; $scope.LOCALE = LOCALE; $scope.close = function close(confirmed) { $uibModalInstance.close(confirmed); }; }, ], }).result.then(function(confirmed) { if (confirmed) { scope.delete(model); } }); } else { scope.model = cleanCopy(scope.model); var elId = scope.field(scope.id + "_file"); var fileEl = angular.element("#" + elId); fileEl.val(null); // Clear the error state. formController[elId].$setValidity("fileSize", true); formController[elId].$setValidity("fileMaxSize", true); formController[elId].$setValidity("filetype", true); formController[elId].$setPristine(); scope.onReset(scope.model); } }; scope.change = function() { scope.onChange(scope.model); }; }, }; }, ] ); } ); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/services/customizeService.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ /* jshint -W089 */ /* jshint -W018 */ define( 'app/services/customizeService',[ // Libraries "angular", "cjt/util/locale", // CJT "cjt/io/api", "cjt/io/whm-v1-request", "app/constants", "cjt/io/whm-v1", // IMPORTANT: Load the driver so its ready "cjt/services/APICatcher", ], function(angular, LOCALE, API, APIREQUEST, CONSTANTS) { "use strict"; var module = angular.module("customize.services.customizeService", [ "cjt2.services.apicatcher", "cjt2.services.api", ]); module.factory("customizeService", ["APICatcher", "$q", function(APICatcher, $q) { // return the factory interface return { /** * @typedef CustomizationModel * @property {Object} brand - properties related to branding a cPanel instanance. * @property {Object} brand.logo - properties related to the logos used in the UI. * @property {string} brand.logo.forLightBackground - base64 encoded logo used when the background color is light. * @property {string} brand.logo.forDarkBackground - base64 encoded logo used when the background color is dark. * @property {string} brand.logo.description - title used with the logo for assistive technology * @property {Object} brand.colors - dictionary of customizable colors for the UI. * @property {string} brand.colors.primary - hex color used in primary UI features. * @property {string} brand.colors.link - hex color used in links. * @property {string} brand.colors.accent - hex color used in accents. * @property {string} brand.favicon - base64 encoded favicon. * @property {Object} help - online help related properties. * @property {string} help.url - URL to the online help for a company. * @property {Object} documentation - documenation related properties. * @property {string} documentation.url - URL to the custom documentation site for a company. */ /** * Update the customization options for jupiter based themes * * @async * @param {CustomizationModel} customizations - the updated customizations to store on the server. * @param {string} theme - the theme name to which the customization is updated. Defaults to CONSTANTS.DEFAULT_THEME. */ update: function(customizations, theme) { if (angular.isUndefined(customizations)) { return $q.reject(LOCALE.maketext("The customization parameter is missing or not an object.")); } var apicall = new APIREQUEST.Class().initialize( "", "update_customizations", { application: "cpanel", theme: theme || CONSTANTS.DEFAULT_THEME, data: JSON.stringify(customizations), }); return APICatcher.promise(apicall); }, /** * Delete a path in the the customization data. * * @async * @param {string} path - optional, The JSONPath to delete * @param {string} theme - the theme name to which the customization is updated. Defaults to CONSTANTS.DEFAULT_THEME. */ delete: function(path, theme) { var apicall = new APIREQUEST.Class().initialize( "", "delete_customizations", { application: "cpanel", theme: theme || CONSTANTS.DEFAULT_THEME, path: path, }); return APICatcher.promise(apicall); }, /** * For the provided tabInfo, this method retrieves the tabs list and their associcated information. * * @param {Object} tabInfo - The theme specific tab information. */ getThemeTabList: function(tabInfo) { var themeTabs = tabInfo.order.map(tab => { return { key: tab, name: CONSTANTS.GENERAL_TABS_INFO[tab], index: tabInfo.index[tab], }; }); return themeTabs; }, }; }]); } ); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/views/jupiter/logoController.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ define( 'app/views/jupiter/logoController',[ "lodash", "angular", "cjt/util/locale", "app/constants", "cjt/directives/autoFocus", "cjt/decorators/growlDecorator", "app/directives/fileUpload", "app/services/customizeService", "app/services/savedService", ], function(_, angular, LOCALE, CONSTANTS) { "use strict"; var module = angular.module("customize.views.logoController", [ "customize.services.customizeService", "customize.directives.fileUpload", "customize.services.savedService", ]); var controller = module.controller( "logoController", [ "$scope", "customizeService", "savedService", "growl", "growlMessages", "PAGE", function($scope, customizeService, savedService, growl, growlMessages, PAGE) { $scope.saving = false; // Load the prefetched data from the PAGE object. var lightLogo = PAGE.data.jupiter.brand.logo.forLightBackground; var darkLogo = PAGE.data.jupiter.brand.logo.forDarkBackground; var description = PAGE.data.jupiter.brand.logo.description; /** * @typedef FileModel * @property {string} filename - name of the file to display * @property {string} data - base 64 encoded file contents * @property {boolean} saved - true if the file has been saved to the backend, false otherwise. * @property {string} name - name of the property */ /** * @typedef LogosModel * @property {FileModel} forLightBackground - storage for the logo use on light backgrounds. * @property {FileModel} forDarkBackground - storage for the logo used on dark backgrounds. * @property {string} description - description for use with the logos as the title property. */ $scope.model = { forLightBackground: { data: lightLogo ? CONSTANTS.EMBEDDED_SVG + lightLogo : "", filename: lightLogo ? "logo-light.svg" : "", saved: !!lightLogo, name: "forLightBackground", }, forDarkBackground: { data: darkLogo ? CONSTANTS.EMBEDDED_SVG + darkLogo : "", filename: darkLogo ? "logo-dark.svg" : "", saved: !!darkLogo, name: "forDarkBackground", }, description: description || "", }; $scope.MAX_FILE_SIZE = CONSTANTS.MAX_FILE_SIZE; $scope.LOCALE = LOCALE; // Watch for changes $scope.$watch("model.forLightBackground", function() { savedService.update("logos", $scope.customization.$dirty); }, true); $scope.$watch("model.forDarkBackground", function() { savedService.update("logos", $scope.customization.$dirty); }, true); $scope.$watch("model.description", function() { savedService.update("logos", $scope.customization.$dirty); }, false); /** * @typedef backgroundColors * @property {string} primaryDark - the background color the logo in full screen. * @property {string} primaryLight - the background color for the logo in mobile. */ $scope.backgroundColors = { primaryDark: PAGE.data.jupiter.brand.colors.primary || CONSTANTS.DEFAULT_PRIMARY_DARK, // NOTE: There does not seem to be an override for mobile background for the logo primaryLight: CONSTANTS.DEFAULT_PRIMARY_LIGHT, }; /** * Save the logo data from the tab. * * @param {FormController} $formCtrl */ $scope.save = function($formCtrl) { growlMessages.destroyAllMessages(); if (!$formCtrl.$valid) { growl.error(LOCALE.maketext("The current customization is invalid.")); return; } if ($scope.saving) { growl.warning(LOCALE.maketext("The system is busy. Try again once the current operation is complete.")); return; } $scope.saving = true; var forDarkBackground = $scope.model.forDarkBackground.data; if (forDarkBackground) { forDarkBackground = forDarkBackground.replace(CONSTANTS.DATA_URL_PREFIX_REGEX, ""); } var forLightBackground = $scope.model.forLightBackground.data; if (forLightBackground) { forLightBackground = forLightBackground.replace(CONSTANTS.DATA_URL_PREFIX_REGEX, ""); } customizeService.update({ brand: { logo: { forLightBackground: forLightBackground, forDarkBackground: forDarkBackground, description: $scope.model.description, }, }, }).then(function(update) { if (forDarkBackground) { $scope.model.forDarkBackground.filename = "logo-dark.svg"; $scope.model.forDarkBackground.saved = true; // Update the initial data PAGE.data.jupiter.brand.logo.forDarkBackground = forDarkBackground; } if (forLightBackground) { $scope.model.forLightBackground.filename = "logo-light.svg"; $scope.model.forLightBackground.saved = true; // Update the initial data PAGE.data.jupiter.brand.logo.forLightBackground = forLightBackground; } // Update the initial data PAGE.data.jupiter.brand.logo.description = $scope.model.description; $formCtrl.$setPristine(); savedService.update("logos", false); growl.success(LOCALE.maketext("The system successfully updated the logos.")); }).catch(function(error) { growl.error(LOCALE.maketext("The system failed to update your logos.")); }).finally(function() { $scope.saving = false; }); }; /** * Evaluate the state of the inputs and update the $pristine state of the form. * * NOTE: * angular.js does not reevalute the form.$isPristine flag when the child inputs * are set to pristine individually. We must loop over the list of controls ourselves * and the set this property. * @param {FormController} $formCtrl */ var updateFormState = function($formCtrl) { var controls = ["file_upload_logo_dark_file", "file_upload_logo_light_file", "icon_description"]; var isPristine = true; // Assume pristine, unless there is evidence otherwise. controls.forEach(function(inputName) { if ($formCtrl[inputName].$dirty) { isPristine = false; } }); if (isPristine) { $formCtrl.$setPristine(); } }; /** * Reset the logo to a pristine state after a delete before saving. * * @param {FormController} $formCtrl * @param {string} which */ $scope.reset = function($formCtrl, which) { growlMessages.destroyAllMessages(); switch (which) { case "forDarkBackground": $formCtrl.file_upload_logo_dark_file.$setPristine(); break; case "forLightBackground": $formCtrl.file_upload_logo_light_file.$setPristine(); break; } updateFormState($formCtrl); savedService.update("logos", $formCtrl.$dirty); }; /** * Remove the specific logo from the * @param {FormController} $formCtrl * @param {string} which - the name of the image field to delete */ $scope.delete = function($formCtrl, which) { growlMessages.destroyAllMessages(); if ($scope.saving) { growl.warning(LOCALE.maketext("The system is busy. Try again once the current operation is complete.")); return; } $scope.saving = true; return customizeService.delete("brand.logo." + which) .then(function(update) { $scope.model[which].saved = false; $scope.model[which].data = ""; $scope.model[which].filename = ""; // Update the initial data PAGE.data.jupiter.brand.logo[which] = ""; // Reset the part of the form that was persisted. switch (which) { case "forDarkBackground": $formCtrl.file_upload_logo_dark_file.$setPristine(); break; case "forLightBackground": $formCtrl.file_upload_logo_light_file.$setPristine(); break; } savedService.update("logos", $formCtrl.$dirty); growl.success(LOCALE.maketext("The system successfully removed the logo.")); }) .catch(function(error) { growl.error(LOCALE.maketext("The system failed to remove the logo.")); }) .finally(function() { $scope.saving = false; }); }; }, ] ); return controller; } ); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/views/jupiter/faviconController.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ define( 'app/views/jupiter/faviconController',[ "lodash", "angular", "cjt/util/locale", "app/constants", "cjt/decorators/growlDecorator", "app/directives/fileUpload", "app/services/customizeService", "app/services/savedService", ], function(_, angular, LOCALE, CONSTANTS) { "use strict"; var module = angular.module("customize.views.faviconController", [ "customize.directives.fileUpload", "customize.services.customizeService", "customize.services.savedService", "customize.directives.fileUpload", ]); // set up the controller var controller = module.controller( "faviconController", [ "$scope", "customizeService", "savedService", "growl", "growlMessages", "PAGE", function($scope, customizeService, savedService, growl, growlMessages, PAGE) { $scope.saving = false; $scope.MAX_FILE_SIZE = CONSTANTS.MAX_FILE_SIZE; $scope.LOCALE = LOCALE; // Load the prefetched data from the PAGE object. var favicon = PAGE.data.jupiter.brand.favicon; /** * @typedef FileModel * @private * @property {string} filename - name of the file to display * @property {string} data - base 64 encoded file contents * @property {boolean} saved - true if the file has been saved to the backend, false otherwise. */ /** * @typedef FaviconModel * @property {FileModel} favicon - storage for the favicon for the site. * @property {FileModel} forDarkBackground - storage for the logo used on dark backgrounds. * @property {string} description - description for use with the logos as the title property. */ $scope.model = { favicon: { data: favicon ? CONSTANTS.EMBEDDED_ICO + favicon : "", filename: favicon ? "favicon.ico" : "", saved: !!favicon, }, }; // Watch for changes $scope.$watch("model.favicon", function() { savedService.update("favicon", $scope.customization.$dirty); }, true); /** * Save the favicon data from the tab. * * @async * @param {FormController} $formCtrl */ $scope.save = function($formCtrl) { growlMessages.destroyAllMessages(); if (!$formCtrl.$valid) { growl.error(LOCALE.maketext("The current customization is invalid.")); return; } if ($scope.saving) { growl.warning(LOCALE.maketext("The system is busy. Try again once the current operation is complete.")); return; } $scope.saving = true; var favicon = $scope.model.favicon.data.replace(CONSTANTS.DATA_URL_PREFIX_REGEX, ""); return customizeService.update({ brand: { favicon: favicon, }, }).then(function(update) { $scope.model.favicon.filename = "favicon.ico"; $scope.model.favicon.saved = true; // Update the initial data PAGE.data.jupiter.brand.favicon = favicon; $formCtrl.$setPristine(); savedService.update("favicon", false); growl.success(LOCALE.maketext("The system successfully updated the favicon.")); }).catch(function(error) { growl.error(LOCALE.maketext("The system failed to update the favicon.")); }).finally(function() { $scope.saving = false; }); }; /** * Reset the favicon to a pristine state after a delete before saving. * * @param {FormController} $formCtrl */ $scope.reset = function($formCtrl) { growlMessages.destroyAllMessages(); $formCtrl.file_upload_favicon_file.$setPristine(); savedService.update("favicons", false); }; /** * Remove the favorite icon from the customizations. * * @param {FormController} $formCtrl - the file to delete from the persistance layer. */ $scope.delete = function($formCtrl) { growlMessages.destroyAllMessages(); if ($scope.saving) { growl.warning(LOCALE.maketext("The system is busy. Try again once the current operation is complete.")); return; } $scope.saving = true; return customizeService.delete("brand.favicon") .then(function(update) { $scope.model.favicon.saved = false; $scope.model.favicon.data = ""; $scope.model.favicon.filename = ""; // Update the initial data PAGE.data.jupiter.brand.favicon = ""; $formCtrl.$setPristine(); savedService.update("favicon", false); growl.success(LOCALE.maketext("The system successfully removed the custom favicon and restored the default [asis,cPanel] favicon.")); }) .catch(function(error) { growl.error(LOCALE.maketext("The system failed to remove the custom favicon.")); }) .finally(function() { $scope.saving = false; }); }; }, ] ); return controller; } ); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/views/jupiter/linksController.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ define('app/views/jupiter/linksController',[ "lodash", "angular", "cjt/util/locale", "cjt/directives/autoFocus", "cjt/decorators/growlDecorator", "app/services/customizeService", "app/services/savedService", ], function(_, angular, LOCALE) { "use strict"; var module = angular.module("customize.views.linksController", [ "customize.services.customizeService", "customize.services.savedService", ]); var controller = module.controller("linksController", [ "$scope", "customizeService", "savedService", "growl", "growlMessages", "PAGE", function( $scope, customizeService, savedService, growl, growlMessages, PAGE ) { $scope.saving = false; $scope.urlRegex = /^(https?):\/\/(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\\d:]+])(?::\\d+)?(?:\/[^?#]*)?(?:\\?[^#]*)?(?:#.*)?$/i; // Preload links $scope.model = { help: PAGE.data.jupiter.help ? angular.copy(PAGE.data.jupiter.help) : { url: "" }, documentation: PAGE.data.jupiter.documentation ? angular.copy(PAGE.data.jupiter.documentation) : { url: "" }, }; // Save initial values $scope.initialHelpLink = $scope.model.help["url"]; $scope.initialDocumentationLink = $scope.model.documentation["url"]; // Watch for changes $scope.$watchGroup(["model.help.url", "model.documentation.url"], function(newValues) { var helpLinkChanged = newValues[0] !== $scope.initialHelpLink; var documentationLinkChanged = newValues[1] !== $scope.initialDocumentationLink; // If the links match their original state, make the form pristine again if (!helpLinkChanged && !documentationLinkChanged) { growlMessages.destroyAllMessages(); savedService.update("links", false); $scope.customization.$setPristine(); } else { savedService.update("links", $scope.customization.$dirty); } }, false); /** * Saves changes to branding customization * Persist the customization form if it is valid * @method save * @param {Object} $formCtrl Form control */ $scope.save = function($formCtrl) { growlMessages.destroyAllMessages(); if (!$formCtrl.$valid) { growl.error( LOCALE.maketext("The current customization is invalid.") ); return; } if ($scope.saving) { growl.warning( LOCALE.maketext( "The system is busy. Try again once the current operation is complete." ) ); return; } $scope.saving = true; customizeService .update({ documentation: { url: $scope.model.documentation.url, }, help: { url: $scope.model.help.url, }, }) .then(function(response) { // For subsequent loads of links tab, we need to update PAGE to reflect changes PAGE.data.jupiter.documentation.url = $scope.model.documentation.url; PAGE.data.jupiter.help.url = $scope.model.help.url; $formCtrl.$setPristine(); savedService.update("links", false); growl.success( LOCALE.maketext( "The system successfully updated your links." ) ); }) .catch(function(error) { growl.error( LOCALE.maketext( "The system failed to update your links." ) ); }) .finally(function() { $scope.saving = false; }); }; }, ]); return controller; }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/views/jupiter/colorsController.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ require.config({ paths: { "jquery-minicolors": "../../../libraries/jquery-minicolors/2.1.7/jquery.minicolors", "angular-minicolors": "../../../libraries/angular-minicolors/0.0.11/angular-minicolors", }, shims: { "jquery-minicolors": { depends: [ "jquery" ], }, "angular-minicolors": { depends: [ "jquery-minicolors" ], }, }, }); define( 'app/views/jupiter/colorsController',[ "lodash", "angular", "jquery", "app/constants", "cjt/util/locale", "cjt/decorators/growlDecorator", "angular-minicolors", ], function(_, angular, jquery, CONSTANTS, LOCALE) { "use strict"; var module = angular.module("customize.views.colorsController", [ "customize.services.customizeService", "minicolors", ]); module.config([ "minicolorsProvider", function(minicolorsProvider) { angular.extend(minicolorsProvider.defaults, { control: "wheel", position: "bottom left", letterCase: "uppercase", theme: "bootstrap", }); }] ); // set up the controller var controller = module.controller( "colorsController", [ "$scope", "customizeService", "growl", "growlMessages", "savedService", "PAGE", function($scope, customizeService, growl, growlMessages, savedService, PAGE) { $scope.saving = false; $scope.restoring = false; $scope.hexColorRegex = "^#[0-9A-Fa-f]{6}"; /** * @typedef ColorBrandPartial * @type {object} * @property {object} brand * @property {ColorsModel} brand.colors */ /** * @typedef ColorsModel * @type {object} * @property {string} primary - CSS color for the left menu * @property {string} link - CSS color for links - NOT IMPLEMENT YET * @property {string} accent - CSS color for various accents in the product. - NOT IMPLEMENT YET */ // Load the prefetched data from the PAGE object. $scope.model = { colors: {}, defaults: angular.copy(CONSTANTS.DEFAULT_COLORS), }; $scope.$watch("model.colors", function() { savedService.update("colors", $scope.customization.$dirty); }, true); /** * Blend the defaults and initial settings to get the current configuration. * * @param {Dictionary<string, string>} initial - initial colors from persistance layer. * @param {Dictionary<string, string>} defaults - default colors for cPanel. * @returns */ function blendColors(initial, defaults) { var copy = Object.assign({}, initial); Object.keys(initial).forEach(function(key) { if (copy[key] === "" || copy[key] === undefined || copy[key] === null) { // Ignore empty keys so we keep the defaults. delete copy[key]; } }); var colors = Object.assign({}, defaults); return Object.assign(colors, copy); } $scope.model.colors = blendColors( PAGE.data.jupiter.brand.colors, CONSTANTS.DEFAULT_COLORS ); /** * Save the updates to the persistance layer. * * @param {FormController} $formCtrl */ $scope.save = function($formCtrl) { if (!$formCtrl.$valid) { growl.error(LOCALE.maketext("The current customization is invalid.")); return; } growlMessages.destroyAllMessages(); if ($scope.saving || $scope.restoring) { growl.warning(LOCALE.maketext("The system is busy. Try again once the current operation is complete.")); return; } $scope.saving = true; /** @type {ColorBrandPartial} */ var partial = { brand: { colors: $scope.model.colors }, }; customizeService.update(partial).then(function(update) { // Update the local init values since we updated the server PAGE.data.jupiter.brand.colors = angular.copy($scope.model.colors); savedService.update("colors", false); $formCtrl.$setPristine(); growl.success(LOCALE.maketext("The system successfully updated the brand colors.")); }).catch(function(error) { growl.error(LOCALE.maketext("The system failed to update the brand colors.")); }).finally(function() { $scope.saving = false; }); }; /** * Remove the brand colors from the customization. * * @param {FormController} $formCtrl */ $scope.reset = function($formCtrl) { growlMessages.destroyAllMessages(); if ($scope.saving || $scope.restoring) { growl.warning(LOCALE.maketext("The system is busy. Try again once the current operation is complete.")); return; } $scope.restoring = true; return customizeService.delete("brand.colors") .then(function(update) { $scope.model.colors = blendColors( {}, CONSTANTS.DEFAULT_COLORS ); // Reset to defaults // Update the local init values since we updated the server PAGE.data.jupiter.brand.colors = angular.copy($scope.model.colors); savedService.update("links", false); $formCtrl.$setPristine(); growl.success(LOCALE.maketext("The system successfully restored the brand colors to the default.")); }) .catch(function(error) { growl.error(LOCALE.maketext("The system failed to restore the brand colors to the default.")); }) .finally(function() { $scope.restoring = false; }); }; }, ] ); return controller; } ); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/services/contactService.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ /* jshint -W089 */ /* jshint -W018 */ define( 'app/services/contactService',[ // Libraries "angular", // CJT "cjt/io/api", "cjt/io/whm-v1-request", "cjt/io/whm-v1", // IMPORTANT: Load the driver so its ready "cjt/services/APICatcher", ], function(angular, API, APIREQUEST) { "use strict"; // Fetch the current application var app = angular.module("customize.services.contactService", [ "cjt2.services.apicatcher", "cjt2.services.api", ]); app.factory("contactService", ["APICatcher", function(APICatcher) { // return the factory interface return { /** * Update the contact data for the company. * * @param {ContactInfo} contactInfo * @returns */ setPublicContact: function(contactInfo) { var apicall = new APIREQUEST.Class().initialize( "", "set_public_contact", contactInfo ); return APICatcher.promise(apicall); }, }; }, ]); }); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/views/publicContactController.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ ( function() { "use strict"; define( 'app/views/publicContactController',[ "lodash", "angular", "cjt/util/locale", "uiBootstrap", "app/services/contactService", "cjt/decorators/growlAPIReporter", "app/services/savedService", ], function(_, angular, LOCALE) { /** * @typedef ContactModel * @property {string} name - the name of the company * @property {string} url - the url to reach the company at. */ // Create the module var app = angular.module( "customize.views.publicContactController", [ "customize.services.contactService", "customize.services.savedService", ] ); app.value("PAGE", PAGE); var PAGEDATA = PAGE.data; // It might be nice for the form model to be saved in this // scope; that way we could restore the form state between // loads of this view. AngularJS, though, doesn’t seem to like // to create FormController objects that are $dirty from the // get-go. We’d have to hook into some sort of post-render event, // and AngularJS *really* seems to want to stay away from that // kind of logic. // var SAVED_PCDATA = angular.copy( PAGEDATA.public_contact ); // Setup the controller var controller = app.controller( "publicContactController", [ "$scope", "contactService", "savedService", "growl", "growlMessages", function($scope, contactService, savedService, growl, growlMessages) { angular.extend( $scope, { has_root: !!PAGEDATA.has_root, pcdata: angular.copy(SAVED_PCDATA), /** * Save the public contacts. * * @param {*} form * @returns */ doSubmit: function doSubmit(form) { var scope = this; growlMessages.destroyAllMessages(); return contactService.setPublicContact(this.pcdata).then( function() { angular.extend(SAVED_PCDATA, scope.pcdata); form.$setPristine(); savedService.update("public-contact", false); growl.success(LOCALE.maketext("The public can now view the information that you provided in this form.")); } ); }, /** * Reset the form to it intial state. */ resetForm: function resetForm(form) { growlMessages.destroyAllMessages(); angular.extend(this.pcdata, SAVED_PCDATA); form.$setPristine(); }, } ); // Watch for changes $scope.$watchGroup([ "pcdata.name", "pcdata.url" ], function() { if (!$scope.public_contact_form) { return; } savedService.update("public-contact", $scope.public_contact_form.$dirty); }, true); }, ] ); return controller; } ); }()); /* # cpanel - whostmgr/docroot/templates/cpanel_customization/index.js # Copyright 2022 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* jshint -W100 */ (function() { "use strict"; define( 'app/index',[ "lodash", "angular", "cjt/core", "cjt/util/locale", "app/constants", "cjt/modules", "uiBootstrap", "cjt/directives/callout", "app/services/savedService", "app/services/beforeUnloadService", // Jupiter Views "app/views/jupiter/logoController", "app/views/jupiter/faviconController", "app/views/jupiter/linksController", "app/views/jupiter/colorsController", // Shared Views "app/views/publicContactController", ], function(_, angular, CJT, LOCALE, CONSTANTS) { return function() { angular.module("App", [ "cjt2.config.whm.configProvider", // This needs to load first "ngRoute", "ui.bootstrap", "angular-growl", "cjt2.whm", "customize.services.savedService", "customize.services.beforeUnloadService", // Jupiter "customize.views.logoController", "customize.views.faviconController", "customize.views.linksController", "customize.views.colorsController", // Shared "customize.views.publicContactController", ]); var app = require( [ "cjt/bootstrap", // Application Modules "uiBootstrap", // Jupiter Views "app/views/jupiter/logoController", "app/views/jupiter/faviconController", "app/views/jupiter/linksController", "app/views/jupiter/colorsController", // Shared Views "app/views/publicContactController", // Services "app/services/contactService", "app/services/customizeService", ], function(BOOTSTRAP) { var app = angular.module("App"); app.value("PAGE", PAGE); app.value("firstLoad", { branding: true, }); app.controller("BaseController", [ "$rootScope", "$scope", "$route", "$location", "growl", "growlMessages", "$timeout", "savedService", "customizeService", function($rootScope, $scope, $route, $location, growl, growlMessages, $timeout, savedService, customizeService) { CONSTANTS.DEFAULT_THEME = PAGE.data.default_theme; $scope.loading = false; $scope.selectedThemeTabList = []; savedService.registerTabs(CONSTANTS.JUPITER_TAB_ORDER); // Convenience functions so we can track changing views for loading purposes $rootScope.$on("$routeChangeStart", function() { if (savedService.needToSave()) { $scope.reportNotSaved(); event.preventDefault(); } $scope.loading = true; }); $rootScope.$on("$routeChangeSuccess", function() { $scope.loading = false; }); $rootScope.$on("$routeChangeError", function() { $scope.loading = false; }); $rootScope.$on("onBeforeUnload", function(e, config) { if (savedService.needToSave()) { config.prompt = LOCALE.maketext("The current tab has unsaved changes. You should save the changes before you navigate to another tab."); e.preventDefault(); return; } delete e["returnValue"]; }); /** * Select a tab by its key. See the indexes for each tab in the ./index.html.tt file. * * @param {number} index */ $scope.selectTab = function(index) { var tabInfo = getTabInfo(); tabInfo.lastTab = index; var activeIndex = tabInfo.index[index]; $scope.activeTab = activeIndex; }; /** * Navigate to the selected path and change the tab being viewed. * * @param {string} path */ $scope.goTo = function(path) { var tabInfo = getTabInfo(); tabInfo.lastTab = path; $scope.selectTab(path); $location.path(path); $scope.selectTab(path); $scope.currentTabName = path; }; /** * Growl a message about not changing tabs. */ $scope.reportNotSaved = function() { growlMessages.destroyAllMessages(); growl.error(LOCALE.maketext("The current tab has unsaved changes. You should save the changes before you navigate to another tab.")); }; /** * Do not let the user navigate away from a tab if there * is unsaved work. */ $scope.preventDeselect = function($event) { if (!$event || !$event.target) { return; } var tabName = findTabName(angular.element($event.target)); if (savedService.needToSave($scope.currentTabName)) { if (tabName !== $scope.currentTabName) { $scope.reportNotSaved(); } $event.preventDefault(); } return; }; /** * Dig thru the els to find the parent with the data-tab-name attribute * * @private * @param {JqLiteHtmlElement} el * @returns {string} */ function findTabName(el) { var name = el.attr("data-tab-name"); if (name) { return name; } var parent = el.parent(); if (parent) { return findTabName(parent); } return; } /** * @typedef ThemeInfo - a set of properties used to configure the tabs for a given theme. * @property {string[]} order - the list of tab names in the order they are shown. * @property {Dictionayr<string,number>} index - the lookup table of tab names to tab indexes. * @property {string} lastTab - the previously selected tab. */ /** * @typedef ThemesInfo - lookup table of tab configuration per theme. * @property {ThemeInfo} jupiter - the jupiter theme configuraiton */ /** * @name byTheme * @scope * @type {ThemesInfo} */ $scope.byTheme = { jupiter: { order: CONSTANTS.JUPITER_TAB_ORDER, index: CONSTANTS.JUPITER_TAB_INDEX, lastTab: "", }, }; $scope.selectedTheme = CONSTANTS.DEFAULT_THEME; /** * Handle theme changes. */ $scope.onThemeSelect = function() { initTab(); }; /** * Retrieve the tab information for the current selected theme. * * @returns {ThemeInfo} */ function getTabInfo() { return $scope.byTheme[$scope.selectedTheme]; } /** * Check if the theme matches the current theme. * * @param {string} themeName * @returns {boolean} true when they are the same, false otherwise. */ $scope.isTheme = function(themeName) { return $scope.selectedTheme === themeName; }; /** * Check if the tab with the tabName is the active tab. * * @param {String} tabName * @returns {boolean} true when the tab is active, false otherwise. */ $scope.isActive = function(tabName) { return $scope.activeTab === tabName; }; /** * Get the active tab name. * * @returns {string} The name of the active tab. */ $scope.getActiveTab = function() { return $scope.activeTab; }; /** * The name of the currently selected tab * @type {string} */ $scope.currentTabName = ""; /** * Select the initial tab. */ function initTab() { var tabInfo = getTabInfo(); $scope.selectedThemeTabList = customizeService.getThemeTabList(tabInfo); var tabName = tabInfo.lastTab || tabInfo.order[0]; $timeout(function() { $scope.goTo(tabName); }); } initTab(); }, ]); app.config(["$routeProvider", function($routeProvider) { // Setup a route - copy this to add additional routes as necessary $routeProvider.when("/logos", { controller: "logoController", templateUrl: CJT.buildFullPath("cpanel_customization/views/jupiter/logo.ptt"), }); $routeProvider.when("/colors", { controller: "colorsController", templateUrl: CJT.buildFullPath("cpanel_customization/views/jupiter/colors.ptt"), }); $routeProvider.when("/links", { controller: "linksController", templateUrl: CJT.buildFullPath("cpanel_customization/views/jupiter/links.ptt"), }); $routeProvider.when("/favicon", { controller: "faviconController", templateUrl: CJT.buildFullPath("cpanel_customization/views/jupiter/favicon.ptt"), }); $routeProvider.when("/public-contact", { controller: "publicContactController", templateUrl: CJT.buildFullPath("cpanel_customization/views/publicContact.ptt"), }); // default route $routeProvider.otherwise({ "redirectTo": "/" + CONSTANTS.DEFAULT_ROUTE, }); }, ]); // Initialize the application BOOTSTRAP(); }); return app; }; } ); })();
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings