partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 8d925cda1cd88d7186638766efa979310baf5f2f
parent 791599d19229c669ddf3271a98f3866c479610a8
Author: Felicitus <felicitus@felicitus.org>
Date:   Sun, 27 Sep 2015 19:42:02 +0200

Refactored frontend authentication system to support WSSE as well as HTTP Basic Auth. Started migration of the TipOfTheDay feature

Diffstat:
Mapp/config/config.yml | 13+++++++++++++
Mapp/config/partkeepr.yml | 1+
Mapp/config/security.yml | 11+++++++++--
Msrc/PartKeepr/AuthBundle/Controller/DefaultController.php | 28++++++++++++++++++++++++++--
Msrc/PartKeepr/AuthBundle/Resources/config/services.xml | 1+
Asrc/PartKeepr/AuthBundle/Security/EntryPoint/NullEntryPoint.php | 24++++++++++++++++++++++++
Msrc/PartKeepr/AuthBundle/Tests/Controller/DefaultControllerTest.php | 2+-
Msrc/PartKeepr/CoreBundle/DependencyInjection/Configuration.php | 27++++++++++++++++-----------
Msrc/PartKeepr/CoreBundle/DependencyInjection/PartKeeprCoreExtension.php | 1+
Msrc/PartKeepr/FrontendBundle/Controller/IndexController.php | 51+++++++--------------------------------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/AuthenticationProvider.js | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/HTTPBasicAuthenticationProvider.js | 20++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/LoginManager.js | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/WSSEAuthenticationProvider.js | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorTree.js | 5+++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartDisplay.js | 7++++---
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js | 29+++++++++++++++++------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/PartMeasurementUnit/PartMeasurementUnitGrid.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Session/SessionManager.js | 77++---------------------------------------------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Statusbar.js | 139+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemNotice/SystemNoticeEditor.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/TipOfTheDay/TipOfTheDayWindow.js | 658+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js | 50++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js | 23++++++++++++++---------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/TipOfTheDayHistoryStore.js | 20++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/TipOfTheDayStore.js | 20++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js | 78++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Util/ServiceCall.js | 4+++-
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 6++++++
Msrc/PartKeepr/TipOfTheDayBundle/Entity/TipOfTheDay.php | 5+++--
Msrc/PartKeepr/TipOfTheDayBundle/Entity/TipOfTheDayHistory.php | 23+++++++++++++++++++++--
32 files changed, 1193 insertions(+), 588 deletions(-)

diff --git a/app/config/config.yml b/app/config/config.yml @@ -956,6 +956,19 @@ services: arguments: - { groups: [ "default" ] } + resource.tip_of_the_day_history: + parent: "api.resource" + arguments: [ "PartKeepr\\TipOfTheDayBundle\\Entity\\TipOfTheDayHistory" ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + resource.user.item_operation.get_preferences: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false diff --git a/app/config/partkeepr.yml b/app/config/partkeepr.yml @@ -1,6 +1,7 @@ partkeepr: image_cache_directory: %kernel.cache_dir%/imagecache/ cronjob_check: false + authentication_provider: PartKeepr.Auth.WSSEAuthenticationProvider directories: iclogo: %kernel.root_dir%/../data/images/iclogo/ temp: %kernel.root_dir%/../data/temp/ diff --git a/app/config/security.yml b/app/config/security.yml @@ -16,17 +16,24 @@ security: chain: providers: [in_memory, fos_userbundle] in_memory: - memory: ~ + memory: + users: + admin: + password: x61Ey612Kl2gpFL56FT9weDnpSo4AV8j8+qx2AuTHdRyY036xxzTTrw10Wq3+4qQyB+XURPWx1ONxp3Y3pB37A== + roles: 'ROLE_ADMIN' fos_userbundle: id: fos_user.user_provider.username firewalls: login: - pattern: ^/auth/getSalt + pattern: ^/api/users/getSalt security: false main: + stateless: true pattern: ^/api/.* provider: fos_userbundle + http_basic: + provider: fos_userbundle wsse: realm: "Secured with WSSE" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate) profile: "UsernameToken" #WSSE profile (WWW-Authenticate) diff --git a/src/PartKeepr/AuthBundle/Controller/DefaultController.php b/src/PartKeepr/AuthBundle/Controller/DefaultController.php @@ -2,6 +2,7 @@ namespace PartKeepr\AuthBundle\Controller; +use Doctrine\ORM\EntityRepository; use FOS\RestBundle\Controller\Annotations\RequestParam; use FOS\RestBundle\Controller\Annotations\View; use FOS\RestBundle\Controller\FOSRestController; @@ -18,7 +19,7 @@ class DefaultController extends FOSRestController /** * Retrieves the salt for a given user * - * @Routing\Route("/auth/getSalt", defaults={"method" = "get","_format" = "json"}) + * @Routing\Route("/api/users/getSalt", defaults={"method" = "post","_format" = "json"}) * @Routing\Method({"POST"}) * @RequestParam(name="username", strict=true, description="The username, 3-50 characters. Allowed characters: a-z, A-Z, 0-9, an underscore (_), a backslash (\), a slash (/), a dot (.) or a dash (-)", requirements=@Username, allowBlank=false) * @View() @@ -32,6 +33,9 @@ class DefaultController extends FOSRestController { $entityManager = $this->getDoctrine()->getManager(); + /** + * @var $repository EntityRepository + */ $repository = $entityManager->getRepository( 'PartKeepr\AuthBundle\Entity\FOSUser' ); @@ -41,6 +45,26 @@ class DefaultController extends FOSRestController */ $user = $repository->findOneBy(array("username" => $paramFetcher->get("username"))); - return $user->getSalt(); + if ($user !== null) { + return $user->getSalt(); + } else { + return false; + } + } + + /** + * Attempts to login the user + * + * @Routing\Route("/api/users/login", defaults={"method" = "post","_format" = "json"}) + * @Routing\Method({"POST"}) + * @View() + * + * @return User The user object + */ + public function loginAction() + { + $user = $this->get("partkeepr.userservice")->getUser(); + + return $user; } } diff --git a/src/PartKeepr/AuthBundle/Resources/config/services.xml b/src/PartKeepr/AuthBundle/Resources/config/services.xml @@ -6,6 +6,7 @@ <parameters> <parameter key="security.authentication.manager.class">PartKeepr\AuthBundle\Security\Authentication\AuthenticationProviderManager</parameter> + <parameter key="security.authentication.basic_entry_point.class">PartKeepr\AuthBundle\Security\EntryPoint\NullEntryPoint</parameter> </parameters> <services> diff --git a/src/PartKeepr/AuthBundle/Security/EntryPoint/NullEntryPoint.php b/src/PartKeepr/AuthBundle/Security/EntryPoint/NullEntryPoint.php @@ -0,0 +1,24 @@ +<?php +namespace PartKeepr\AuthBundle\Security\EntryPoint; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + +/** + * Implements a NullEntryPoint to avoid that the user is being asked for their password in HTTP Basic Auth Scenarios + */ +class NullEntryPoint implements AuthenticationEntryPointInterface +{ + /** + * {@inheritdoc} + */ + public function start(Request $request, AuthenticationException $authException = null) + { + $response = new Response(); + $response->setStatusCode(401); + + return $response; + } +} diff --git a/src/PartKeepr/AuthBundle/Tests/Controller/DefaultControllerTest.php b/src/PartKeepr/AuthBundle/Tests/Controller/DefaultControllerTest.php @@ -31,7 +31,7 @@ class DefaultControllerTest extends WebTestCase $client->request( 'POST', - '/auth/getSalt', + '/api/users/getSalt', array(), array(), array('CONTENT_TYPE' => 'application/json'), diff --git a/src/PartKeepr/CoreBundle/DependencyInjection/Configuration.php b/src/PartKeepr/CoreBundle/DependencyInjection/Configuration.php @@ -15,19 +15,24 @@ class Configuration implements ConfigurationInterface $rootNode = $treeBuilder->root('partkeepr'); $rootNode-> - children() - ->scalarNode('image_cache_directory') - ->cannotBeEmpty() - ->isRequired() - ->info('The image cache directory') - ->end() + children() + ->scalarNode('authentication_provider') + ->cannotBeEmpty() + ->defaultValue('PartKeepr.Auth.WSSEAuthenticationProvider') + ->info('The authentication provider for the frontend') + ->end() + ->scalarNode('image_cache_directory') + ->cannotBeEmpty() + ->isRequired() + ->info('The image cache directory') + ->end() ->arrayNode('directories') - ->prototype('scalar') - ->end() + ->prototype('scalar') ->end() - ->booleanNode('cronjob_check') - ->defaultTrue() - ->info('Whether the system should check if cronjobs are running or not') + ->end() + ->booleanNode('cronjob_check') + ->defaultTrue() + ->info('Whether the system should check if cronjobs are running or not') ->end() ->end(); diff --git a/src/PartKeepr/CoreBundle/DependencyInjection/PartKeeprCoreExtension.php b/src/PartKeepr/CoreBundle/DependencyInjection/PartKeeprCoreExtension.php @@ -24,6 +24,7 @@ class PartKeeprCoreExtension extends Extension $container->setParameter('partkeepr.cronjob_check', $config['cronjob_check']); $container->setParameter('partkeepr.image_cache_directory', $config['image_cache_directory']); + $container->setParameter('partkeepr.authentication_provider', $config['authentication_provider']); foreach ($config["directories"] as $key => $value) { $container->setParameter("partkeepr.directories.".$key, $value); diff --git a/src/PartKeepr/FrontendBundle/Controller/IndexController.php b/src/PartKeepr/FrontendBundle/Controller/IndexController.php @@ -2,7 +2,10 @@ namespace PartKeepr\FrontendBundle\Controller; +use Doctrine\Common\Version as DoctrineCommonVersion; +use Doctrine\DBAL\Version as DBALVersion; use Doctrine\ORM\NoResultException; +use Doctrine\ORM\Version as ORMVersion; use PartKeepr\AuthBundle\Entity\User; use PartKeepr\PartKeepr; use PartKeepr\Session\SessionManager; @@ -11,9 +14,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Routing\Annotation\Route; -use Doctrine\ORM\Version as ORMVersion; -use Doctrine\DBAL\Version as DBALVersion; -use Doctrine\Common\Version as DoctrineCommonVersion; class IndexController extends Controller { @@ -25,21 +25,19 @@ class IndexController extends Controller { PartKeepr::initialize(""); - $this->legacyAuthStuff(); - $aParameters = array(); $aParameters["doctrine_orm_version"] = ORMVersion::VERSION; $aParameters["doctrine_dbal_version"] = DBALVersion::VERSION; $aParameters["doctrine_common_version"] = DoctrineCommonVersion::VERSION; $aParameters["php_version"] = phpversion(); + $aParameters["auto_start_session"] = true; $maxPostSize = PartKeepr::getBytesFromHumanReadable(ini_get("post_max_size")); $maxFileSize = PartKeepr::getBytesFromHumanReadable(ini_get("upload_max_filesize")); $aParameters["maxUploadSize"] = min($maxPostSize, $maxFileSize); - if (!class_exists("Imagick")) - { + if (!class_exists("Imagick")) { // @todo This check is deprecated and shouldn't be done here. Sf2 should automatically take care of this return $this->render('PartKeeprFrontendBundle::error.html.twig', @@ -66,6 +64,8 @@ class IndexController extends Controller $aParameters["motd"] = Configuration::getOption("partkeepr.frontend.motd"); } + $aParameters["authentication_provider"] = $this->getParameter("partkeepr.authentication_provider"); + $renderParams = array(); $renderParams["debug_all"] = Configuration::getOption("partkeepr.frontend.debug_all", false); $renderParams["debug"] = Configuration::getOption("partkeepr.frontend.debug", false); @@ -108,41 +108,4 @@ class IndexController extends Controller return $models; } - - protected function legacyAuthStuff() - { - /* HTTP auth */ - if (Configuration::getOption("partkeepr.auth.http", false) === true) { - if (!isset($_SERVER["PHP_AUTH_USER"])) { - // @todo Redirect to permission denied page - die("Permission denied"); - } - - try { - $user = User::loadByName($_SERVER['PHP_AUTH_USER']); - } catch (NoResultException $e) { - $user = new User; - $user->setUsername($_SERVER['PHP_AUTH_USER']); - $user->setPassword("invalid"); - - PartKeepr::getEM()->persist($user); - PartKeepr::getEM()->flush(); - } - - - $session = SessionManager::getInstance()->startSession($user); - - $aParameters["autoLoginUsername"] = $user->getUsername(); - $aParameters["auto_start_session"] = $session->getSessionID(); - - $aPreferences = array(); - - foreach ($user->getPreferences() as $result) { - $aPreferences[] = $result->serialize(); - } - - $aParameters["userPreferences"] = array("response" => array("data" => $aPreferences)); - } - - } } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/AuthenticationProvider.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/AuthenticationProvider.js @@ -0,0 +1,158 @@ +/** + * Base class for authentication providers + */ +Ext.define('PartKeepr.Auth.AuthenticationProvider', { + + mixins: ['Ext.mixin.Observable'], + + /** + * @var {String} The username + */ + username: null, + + /** + * @var {String} The password + */ + password: null, + + /** + * @var {Object} The authenaticated user + */ + user: null, + + constructor: function (config) + { + this.mixins.observable.constructor.call(this, config); + }, + + /** + * Returns any additional headers for the requests. + * + * Must be overriden in the child classes. + * + * @return {Object} An object with all additional headers + */ + getHeaders: function () + { + return {}; + }, + + /** + * Sets the username for authentication + * + * @param {String} username The username + */ + setUsername: function (username) + { + this.username = username; + }, + + /** + * Returns the username for authentication + * + * @return {String} The username + */ + getUsername: function () + { + return this.username; + }, + + /** + * Sets the password for authentication + * + * @param {String} password The password + */ + setPassword: function (password) + { + this.password = password; + }, + + /** + * Returns the password for authentication + * + * @return {String} The password + */ + getPassword: function () + { + return this.password; + }, + + /** + * Triggers the authentication. By default, this simply calls the /api/users/login action, but + * can be overriden in child classes to provide advanced logic. + */ + authenticate: function () + { + PartKeepr.AuthBundle.Entity.User.callPostCollectionAction("login", + {}, + Ext.bind(this.onLogin, this), + true + ); + }, + /** + * Sets the user object + * + * @var {Object} user The user object + */ + setUser: function (user) + { + this.user = user; + }, + /** + * Returns the user object + * + * @return {Object} The user object + */ + getUser: function () + { + return this.user; + }, + /** + * Callback handler for the login action. Checks if the response contains a status code of 401. + * + * @param {Object} options The options object + * @param {Boolean} success If the request was successful + * @param {Object} response The response object + */ + onLogin: function (options, success, response) + { + if (response.status == "401") { + this.fireEvent("authenticate", false); + } else { + this.setUser(Ext.decode(response.responseText)); + this.fireEvent("authenticate", true); + } + }, + statics: { + /** + * @var {Object} The current authentication provider + */ + authenticationProvider: null, + + /** + * Retrieves the authentication provider. If no authentication provider is set, automatically + * returns the base class, which doesn't have any functionality. + * + * @return {Object} The authentication provider + */ + getAuthenticationProvider: function () + { + if (!(this.authenticationProvider instanceof PartKeepr.Auth.AuthenticationProvider)) { + this.authenticationProvider = Ext.create("PartKeepr.Auth.AuthenticationProvider"); + } + + return this.authenticationProvider; + }, + + /** + * Sets the authentication provider + * + * @param {Object} The authentication provider + */ + setAuthenticationProvider: function (authenticationProvider) + { + this.authenticationProvider = authenticationProvider; + + } + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/HTTPBasicAuthenticationProvider.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/HTTPBasicAuthenticationProvider.js @@ -0,0 +1,20 @@ +/** + * HTTP Basic Authentication Provider + */ +Ext.define('PartKeepr.Auth.HTTPBasicAuthenticationProvider', { + extend: 'PartKeepr.Auth.AuthenticationProvider', + + /** + * @method add + * @inheritdoc PartKeepr.Auth.AuthenticationProvider#getHeaders + */ + getHeaders: function () + { + var hash = base64_encode(this.getUsername() + ":" + this.getPassword()); + + return { + "Authorization": "Basic " + hash + }; + + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/LoginManager.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/LoginManager.js @@ -0,0 +1,126 @@ +/** + * The login manager is responsible for handling logins. Depending on the configuration, the user may be + * pre-authenticated or the system needs to display a login dialog. + */ +Ext.define('PartKeepr.Auth.LoginManager', { + + mixins: ['Ext.mixin.Observable'], + + /** + * @var {Object} An instance of the login dialog + */ + loginDialog: null, + + /** + * @var {Object} The authentication provider + */ + provider: null, + + /** + * @var {Boolean} If the user is logged in or not + */ + loggedIn: false, + + config: { + /** + * @var {Boolean} True if auto-login is required + */ + autoLogin: false, + + /** + * @var {String} The username to use for auto-login + */ + autoLoginUsername: null, + + /** + * @var {String} The password to use for auto-login + */ + autoLoginPassword: null + }, + + constructor: function (config) + { + this.mixins.observable.constructor.call(this, config); + this.provider = PartKeepr.Auth.AuthenticationProvider.getAuthenticationProvider(); + this.provider.on("authenticate", this.onAuthenticate, this); + + this.loginDialog = Ext.create("PartKeepr.LoginDialog"); + this.loginDialog.on("login", this.onLoginDialog, this); + }, + /** + * Triggers the login process. If auto-login is required, directly calls authenticate(). If not, the + * login dialog is shown. + */ + login: function () + { + if (this.config.autoLogin) { + this.provider.setUsername(this.config.autoLoginUsername); + this.provider.setPassword(this.config.autoLoginPassword); + this.provider.authenticate(); + } else { + this.loginDialog.show(); + } + }, + /** + * Triggers the logout process. + */ + logout: function () + { + this.loggedIn = false; + this.fireEvent("logout"); + }, + /** + * Callback when the authentication has completed. Fires the "login" event if the authentication was successful. + * Displays an error message if the authentication was not successful. + * + * @param {Boolean} success If the authentication was successful or not + */ + onAuthenticate: function (success) + { + if (success) { + this.loginDialog.hide(); + this.fireEvent("login"); + this.loggedIn = true; + } else { + Ext.Msg.alert(i18n("Error"), i18n('Username or password invalid.'), + function () + { + this.loginDialog.show(); + }, + this + ); + } + }, + /** + * Returns the authenticated user + * + * @return {Object} The user object + */ + getUser: function () + { + return this.provider.getUser(); + }, + /** + * Callback when the login dialog fired the "login" event. Passes the login data to the authentication provider + * and starts the authentication process + * + * @param {String} username The username + * @param {String} password The password + */ + onLoginDialog: function (username, password) + { + this.provider.setUsername(username); + this.provider.setPassword(password); + this.provider.authenticate(); + }, + /** + * Returns if the user is logged in or not + * + * @return {Boolean} + */ + isLoggedIn: function () + { + return this.loggedIn; + } + +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/WSSEAuthenticationProvider.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Auth/WSSEAuthenticationProvider.js @@ -0,0 +1,168 @@ +/** + * WSSE Authentication Provider + */ +Ext.define('PartKeepr.Auth.WSSEAuthenticationProvider', { + extend: 'PartKeepr.Auth.AuthenticationProvider', + + /** + * @var {String} The WSSE secret + */ + secret: null, + + /** + * @var {String} The user's salt + */ + salt: null, + + /** + * Retrieves the salt for the user. Note that the authentication for WSSE is a two-part process: + * In order to authenticate, we require the salt first to build the password hash. + */ + authenticate: function () + { + PartKeepr.AuthBundle.Entity.User.callPostCollectionAction("getSalt", + { + username: this.getUsername() + }, + Ext.bind(this.onSaltRetrieved, this) + ); + }, + + /** + * Callback when the salt was received. Generates the secret and attempts to login the user. + * + * @param {Object} options + * @param {Object} success + * @param {Object} response + */ + onSaltRetrieved: function (options, success, response) + { + this.salt = Ext.decode(response.responseText); + + this.generateSecret(); + + PartKeepr.AuthBundle.Entity.User.callPostCollectionAction("login", + {}, + Ext.bind(this.onLogin, this), + true + ); + + }, + + /** + * @method add + * @inheritdoc PartKeepr.Auth.AuthenticationProvider#getHeaders + */ + getHeaders: function () + { + if (this.secret !== null) { + return {"X-WSSE": this.getWSSE()}; + } + }, + + /** + * Generates the WSSE Secret + */ + generateSecret: function () + { + this.secret = CryptoJS.enc.Base64.stringify(CryptoJS.SHA512(this.getPassword() + "{" + this.salt + "}")); + }, + + /** + * Generates the nonce + * + * @param {Integer} length The length of the nonce + * @return {String} The generated nonce + */ + generateNonce: function (length) + { + var nonceChars = "0123456789abcdef"; + var result = ""; + for (var i = 0; i < length; i++) { + result += nonceChars.charAt(Math.floor(Math.random() * nonceChars.length)); + } + return result; + }, + + /** + * Returns a W3C-Compliant date + * + * @param {Object} date The DateTime object to convert + * @return {String} The W3C-compliant date + */ + getW3CDate: function (date) + { + var yyyy = date.getUTCFullYear(); + var mm = (date.getUTCMonth() + 1); + if (mm < 10) { + mm = "0" + mm; + } + var dd = (date.getUTCDate()); + if (dd < 10) { + dd = "0" + dd; + } + var hh = (date.getUTCHours()); + if (hh < 10) { + hh = "0" + hh; + } + var mn = (date.getUTCMinutes()); + if (mn < 10) { + mn = "0" + mn; + } + var ss = (date.getUTCSeconds()); + if (ss < 10) { + ss = "0" + ss; + } + return yyyy + "-" + mm + "-" + dd + "T" + hh + ":" + mn + ":" + ss + "Z"; + }, + + /** + * Returns the WSSE string for authentication + * + * @return {String} + */ + getWSSE: function () + { + var nonce = this.generateNonce(16); + var nonce64 = base64_encode(nonce); + var created = this.getW3CDate(new Date()); + + var digest = this.encodePassword(nonce + created + this.secret, this.salt, 1); + return "UsernameToken Username=\"" + + this.getUsername() + "\", PasswordDigest=\"" + + digest + "\", Nonce=\"" + + nonce64 + "\", Created=\"" + + created + "\""; + }, + + /** + * Merges the password and salt + * + * @param {String} raw The raw password + * @param {String} salt The salt + */ + mergePasswordAndSalt: function (raw, salt) + { + return raw + "{" + salt + "}"; + }, + + /** + * Encodes the password with the salt and a specific number of iterations + * + * @param {String} raw The raw password + * @param {String} salt The salt + * @param {Integer} iterations The number of iterations + */ + encodePassword: function (raw, salt, iterations) + { + var salted = this.mergePasswordAndSalt(raw, salt); + + var digest = CryptoJS.SHA512(salted); + + for (var i = 1; i < digest; i++) { + digest = CryptoJS.SHA512(digest + salted); + } + + return CryptoJS.enc.Base64.stringify(digest); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorTree.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorTree.js @@ -32,7 +32,8 @@ Ext.define("PartKeepr.CategoryEditorTree", { this.createMenu(); }, - onBeforeDrop: function (node, data, overModel, dropPosition, dropHandlers) { + onBeforeDrop: function (node, data, overModel, dropPosition, dropHandlers) + { var draggedRecord = data.records[0]; var droppedOn = this.getView().getRecord(node); @@ -57,7 +58,7 @@ Ext.define("PartKeepr.CategoryEditorTree", { targetRecord = overModel; } - draggedRecord.callAction("move", { + draggedRecord.callPutAction("move", { "parent": targetRecord.getId() }); } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartDisplay.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartDisplay.js @@ -151,7 +151,8 @@ Ext.define('PartKeepr.PartDisplay', { ]; this.callParent(); }, - clear: function () { + clear: function () + { this.attachmentDisplay.bindStore(null); this.imageDisplay.setStore(null); @@ -203,7 +204,7 @@ Ext.define('PartKeepr.PartDisplay', { */ addPartHandler: function (quantity, price, comment) { - this.record.callAction("addStock", { + this.record.callPutAction("addStock", { quantity: quantity, price: price, comment: comment @@ -222,7 +223,7 @@ Ext.define('PartKeepr.PartDisplay', { */ deletePartHandler: function (quantity) { - this.record.callAction("removeStock", { + this.record.callPutAction("removeStock", { quantity: quantity, }, null, true); }, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js @@ -117,8 +117,10 @@ Ext.define('PartKeepr.PartsGrid', { } }); - var duplicateBasicData = i18n("Duplicates the selected part with the data found in the \"basic\" tab and opens the editor. Doesn't immediately saves the duplicate, in order to allow editing."); - var duplicateAllData = i18n("Duplicates the selected part with all data including attachments, distributors etc. Doesn't immediately saves the duplicate, in order to allow editing."); + var duplicateBasicData = i18n( + "Duplicates the selected part with the data found in the \"basic\" tab and opens the editor. Doesn't immediately saves the duplicate, in order to allow editing."); + var duplicateAllData = i18n( + "Duplicates the selected part with all data including attachments, distributors etc. Doesn't immediately saves the duplicate, in order to allow editing."); this.addFromTemplateButton = Ext.create("Ext.button.Split", { disabled: true, @@ -295,7 +297,8 @@ Ext.define('PartKeepr.PartsGrid', { ]; }, - averagePriceRenderer: function () { + averagePriceRenderer: function () + { "use strict"; return 0; }, @@ -347,7 +350,8 @@ Ext.define('PartKeepr.PartsGrid', { { var ret = ""; if (rec.attachments().getCount() > 0) { - ret += '<img src="bundles/brainbitsfugueicons/icons/fugue/16/paper-clip.png" style="height: 10px; margin-top: 2px;" alt="' + i18n("Has attachments") + '" title="' + i18n("Has attachments") + '"/>'; + ret += '<img src="bundles/brainbitsfugueicons/icons/fugue/16/paper-clip.png" style="height: 10px; margin-top: 2px;" alt="' + i18n( + "Has attachments") + '" title="' + i18n("Has attachments") + '"/>'; } return ret; @@ -413,7 +417,8 @@ Ext.define('PartKeepr.PartsGrid', { this.confirmStockChange(e); } }, - getStockChangeMode: function (value) { + getStockChangeMode: function (value) + { var n = value.indexOf("+"); if (n !== -1) { @@ -459,16 +464,16 @@ Ext.define('PartKeepr.PartsGrid', { i18n("You wish to add <b>%s %s</b> of part <b>%s</b>. Is this correct?"), value, e.record.getPartUnit().get("name"), e.record.get("name")); - e.record.set("stockLevel", (e.originalValue + value)); - headerText = i18n("Add Part(s)"); - break; + e.record.set("stockLevel", (e.originalValue + value)); + headerText = i18n("Add Part(s)"); + break; case "fixed": - confirmText = sprintf( + confirmText = sprintf( i18n("You wish to set the stock level to <b>%s %s</b> for part <b>%s</b>. Is this correct?"), value, e.record.getPartUnit().get("name"), e.record.get("name")); - e.record.set("stockLevel", value); - headerText = i18n("Set Stock Level for Part(s)"); + e.record.set("stockLevel", value); + headerText = i18n("Set Stock Level for Part(s)"); break; } @@ -532,7 +537,7 @@ Ext.define('PartKeepr.PartsGrid', { break; } - e.record.callAction(call, { + e.record.callPutAction(call, { quantity: e.value }, Ext.bind(this.reloadPart, this, [e])); }, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/PartMeasurementUnit/PartMeasurementUnitGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/PartMeasurementUnit/PartMeasurementUnitGrid.js @@ -52,7 +52,7 @@ Ext.define('PartKeepr.PartMeasurementUnitGrid', { { var r = this.getSelectionModel().getLastSelected(); - r.callAction("setDefault", {}, this.onDefaultHandler.bind(this)); + r.callPutAction("setDefault", {}, this.onDefaultHandler.bind(this)); }, onDefaultHandler: function () { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js @@ -281,7 +281,7 @@ Ext.define('PartKeepr.ProjectReportView', { }); } - PartKeepr.PartBundle.Entity.Part.callCollectionAction("massRemoveStock", + PartKeepr.PartBundle.Entity.Part.callPostCollectionAction("massRemoveStock", {"removals": Ext.encode(removals)}); } }, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Session/SessionManager.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Session/SessionManager.js @@ -28,40 +28,7 @@ Ext.define("PartKeepr.SessionManager", { { this.callParent(arguments); }, - generateNonce: function (length) - { - var nonceChars = "0123456789abcdef"; - var result = ""; - for (var i = 0; i < length; i++) { - result += nonceChars.charAt(Math.floor(Math.random() * nonceChars.length)); - } - return result; - }, - getW3CDate: function (date) - { - var yyyy = date.getUTCFullYear(); - var mm = (date.getUTCMonth() + 1); - if (mm < 10) { - mm = "0" + mm; - } - var dd = (date.getUTCDate()); - if (dd < 10) { - dd = "0" + dd; - } - var hh = (date.getUTCHours()); - if (hh < 10) { - hh = "0" + hh; - } - var mn = (date.getUTCMinutes()); - if (mn < 10) { - mn = "0" + mn; - } - var ss = (date.getUTCSeconds()); - if (ss < 10) { - ss = "0" + ss; - } - return yyyy + "-" + mm + "-" + dd + "T" + hh + ":" + mn + ":" + ss + "Z"; - }, + /** * Creates and shows the login dialog, as well as setting up any event handlers. */ @@ -76,45 +43,6 @@ Ext.define("PartKeepr.SessionManager", { this.loginDialog.show(); } }, - onSaltRetrieved: function (salt) - { - this.salt = salt; - - - this.secret = CryptoJS.enc.Base64.stringify(CryptoJS.SHA512(this.password + "{" + this.salt + "}")); - this.loginDialog.destroy(); - this.fireEvent("login"); - }, - getWSSE: function () { - var nonce = this.generateNonce(16); - var nonce64 = base64_encode(nonce); - var created = this.getW3CDate(new Date()); - - var digest = this.encodePassword(nonce + created + this.secret, this.salt, 1); - return "UsernameToken Username=\"" - + this.username + "\", PasswordDigest=\"" - + digest + "\", Nonce=\"" - + nonce64 + "\", Created=\"" - + created + "\""; - }, - mergePasswordAndSalt: function (raw, salt) - { - "use strict"; - return raw + "{" + salt + "}"; - }, - encodePassword: function (raw, salt, iterations) - { - "use strict"; - var salted = this.mergePasswordAndSalt(raw, salt); - - var digest = CryptoJS.SHA512(salted); - - for (var i = 1; i < digest; i++) { - digest = CryptoJS.SHA512(digest + salted); - } - - return CryptoJS.enc.Base64.stringify(digest); - }, /** * Removes the current session. */ @@ -174,4 +102,4 @@ Ext.define("PartKeepr.SessionManager", { { return this.session; } -});- \ No newline at end of file +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Statusbar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Statusbar.js @@ -1,70 +1,89 @@ Ext.define('PartKeepr.Statusbar', { - extend: 'Ext.ux.statusbar.StatusBar', - - defaultText: i18n("Ready."), - defaultIconCls: 'x-status-valid', - iconCls: 'x-status-valid', - autoClear: 3000, - initComponent: function () { - this.connectionButton = new PartKeepr.ConnectionButton(); - this.connectionButton.on("click", this.onConnectionButtonClick, this); - this.timeDisplay = Ext.create("PartKeepr.TimeDisplay"); + extend: 'Ext.ux.statusbar.StatusBar', + + defaultText: i18n("Ready."), + defaultIconCls: 'x-status-valid', + iconCls: 'x-status-valid', + autoClear: 3000, + initComponent: function () + { + this.connectionButton = new PartKeepr.ConnectionButton(); + this.connectionButton.on("click", this.onConnectionButtonClick, this); + this.timeDisplay = Ext.create("PartKeepr.TimeDisplay"); this.currentUserDisplay = Ext.create("Ext.toolbar.TextItem"); - this.currentUserDisplay.setText(i18n("Not logged in")); + this.showMessageLog = Ext.create("Ext.Button", { + glyph: 0xf120, + cls: 'x-btn-icon', + handler: function () + { + PartKeepr.getApplication().toggleMessageLog(); + } + }); + + this.systemNoticeButton = Ext.create("PartKeepr.SystemNoticeButton", { + hidden: true + }); + + Ext.apply(this, { + items: [ + this.currentUserDisplay, + {xtype: 'tbseparator'}, + this.timeDisplay, + {xtype: 'tbseparator'}, + this.showMessageLog, + {xtype: 'tbseparator'}, + this.connectionButton, + this.systemNoticeButton - this.showMessageLog = Ext.create("Ext.Button",{ - glyph: 0xf120, - cls: 'x-btn-icon', - handler: function () { - PartKeepr.getApplication().toggleMessageLog(); - } - }); + ] + }); - this.systemNoticeButton = Ext.create("PartKeepr.SystemNoticeButton", { - hidden: true - }); + this.setDisconnected(); - Ext.apply(this, { - items: [ - this.currentUserDisplay, - {xtype: 'tbseparator'}, - this.timeDisplay, - { xtype: 'tbseparator' }, - this.showMessageLog, - { xtype: 'tbseparator' }, - this.connectionButton, - this.systemNoticeButton + this.callParent(); + }, + getConnectionButton: function () + { + return this.connectionButton; + }, + setCurrentUser: function (username) + { + this.currentUserDisplay.setText(i18n("Logged in as") + ": " + username); + }, + startLoad: function (message) + { + if (message !== null) { + this.showBusy({text: message, iconCls: "x-status-busy"}); + } else { + this.showBusy(); + } + }, + endLoad: function () + { + this.clearStatus({useDefaults: true}); + }, + setConnected: function () + { + var user = PartKeepr.getApplication().getLoginManager().getUser(); - ] - }); - - - this.callParent(); - }, - getConnectionButton: function () { - return this.connectionButton; - }, - setCurrentUser: function (username) { - this.currentUserDisplay.setText(i18n("Logged in as")+": "+username); - }, - startLoad: function (message) { - if (message !== null) { - this.showBusy({text: message, iconCls: "x-status-busy"}); - } else { - this.showBusy(); - } - }, - endLoad: function () { - this.clearStatus({useDefaults: true}); - }, - onConnectionButtonClick: function () { - if (PartKeepr.getApplication().getSession()) { - PartKeepr.getApplication().logout(); - } else { - PartKeepr.getApplication().getSessionManager().login(); - } - } + this.setCurrentUser(user.username); + this.connectionButton.setConnected(); + }, + setDisconnected: function () + { + this.connectionButton.setDisconnected(); + this.currentUserDisplay.setText(i18n("Not logged in")); + }, + onConnectionButtonClick: function () + { + var loginManager = PartKeepr.getApplication().getLoginManager(); + if (loginManager.isLoggedIn()) { + loginManager.logout(); + } else { + loginManager.login(); + } + } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemNotice/SystemNoticeEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemNotice/SystemNoticeEditor.js @@ -67,7 +67,7 @@ Ext.define('PartKeepr.SystemNoticeEditor', { }, onAcknowledgeClick: function () { - this.record.callAction("acknowledge", [], Ext.bind(this.onAcknowledged, this)); + this.record.callPutAction("acknowledge", [], Ext.bind(this.onAcknowledged, this)); }, onAcknowledged: function () { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/TipOfTheDay/TipOfTheDayWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/TipOfTheDay/TipOfTheDayWindow.js @@ -2,315 +2,348 @@ * This class represents the tip of the day window and its logic. */ Ext.define("PartKeepr.TipOfTheDayWindow", { - extend: 'Ext.window.Window', - - /* Defines the title template. */ - titleTemplate: i18n("Tip of the Day"), - - /* Cosmetic settings */ - width: 600, - height: 300, - - minWidth: 600, - minHeight: 300, - - layout: 'fit', - - /** - * Stores the currently displayed tip, or null if none is displayed - * @var Ext.data.Record - */ - currentTip: null, - - /** - * Holds an instance of the TipOfTheDay store. - */ - tipStore: null, - - /** - * Initializes the window. Adds the iframe used for displaying tips, as well - * as the user controls (prev/next buttons, config checkboxes). - */ - initComponent: function () { - // Initialize the window with the title template - this.title = this.titleTemplate; - - // Set the tip store - this.tipStore = PartKeepr.getApplication().getTipOfTheDayStore(); - - // Set the tip display iframe and add it to the items - this.tipDisplay = Ext.create("Ext.ux.SimpleIFrame", { - border: false - }); - - this.items = this.tipDisplay; - - // Initialize previous and next buttons - this.previousButton = Ext.create("Ext.button.Button", { - text: i18n("Previous Tip"), - handler: Ext.bind(this.displayPreviousTip, this), - icon: 'bundles/partkeeprfrontend/images/icons/tip_previous.png', - disabled: true - }); - - this.nextButton = Ext.create("Ext.button.Button", { - text: i18n("Next Tip"), - icon: 'bundles/partkeeprfrontend/images/icons/tip_next.png', - handler: Ext.bind(this.displayNextTip, this) - }); - - // Initializes the "show tips on login" checkbox as well as the "show read tips" checkbox - this.showTipsCheckbox = Ext.create("Ext.form.field.Checkbox", { - boxLabel: i18n("Show Tips on login"), - handler: Ext.bind(this.showTipsHandler, this) - }); - - this.displayReadTipsCheckbox = Ext.create("Ext.form.field.Checkbox", { - boxLabel: i18n("Show read tips"), - handler: Ext.bind(this.showReadTipsHandler, this) - }); - - // Initialize the "show tips" checkbox with the user preference - if (PartKeepr.getApplication().getUserPreference("partkeepr.tipoftheday.showtips") === false) { - this.showTipsCheckbox.setValue(false); - } else { - this.showTipsCheckbox.setValue(true); - } - - // Append the controls to the bottom toolbar - this.dockedItems = [{ - xtype: 'toolbar', - dock: 'bottom', - ui: 'footer', - defaults: {minWidth: 100}, - pack: 'start', - items: [ - this.previousButton, - this.nextButton, - '->', - this.showTipsCheckbox, - this.displayReadTipsCheckbox - ] - }]; - - // Auto-load the next unread tip on window display - this.on("show", this.displayNextTip, this); - - // Window destroy handler - this.on("destroy", this.onDestroy, this); - this.callParent(); - }, - /** - * If the "show read tips" checkbox was clicked, update the buttons - * to reflect the tip navigation. - */ - showReadTipsHandler: function () { - this.updateButtons(this.currentTip); - }, - /** - * Destroy handler. Removes the "read tip" timer. - */ - onDestroy: function () { - this.cancelReadTimer(); - }, - /** - * Cancels the read timer. - */ - cancelReadTimer: function () { - if (this.markAsReadTask) { - this.markAsReadTask.cancel(); - } - }, - /** - * Handler when the "show tips" checkbox was clicked. - */ - showTipsHandler: function (checkbox, checked) { - PartKeepr.getApplication().setUserPreference("partkeepr.tipoftheday.showtips", checked); - }, - /** - * Displays a specific tip of the day. - * @param record The record which contains the information regarding the tip - */ - displayTip: function (record) { - // Cancel the old read timer - this.cancelReadTimer(); - - // Update buttons to reflect position - this.updateButtons(record); - - // Set the title to the tip name - this.setTitle(this.titleTemplate+ ": " + record.get("name")); - - // Set iframe to the tip url - this.tipDisplay.setSrc(record.get("url")); - - // Fire up delayed task to mark the tip as read - this.markAsReadTask = new Ext.util.DelayedTask(this.markTipRead, this); - this.markAsReadTask.delay(5000); - - }, - /** - * Updates the navigation buttons. - * - * This method has two modes, depending on which state the "show read tips" checkbox is in. - * @param record The currently displayed tip - */ - updateButtons: function (record) { - if (this.displayReadTipsCheckbox.getValue() === true) { - if (this.tipStore.indexOf(record) > 0) { - this.previousButton.enable(); - } else { - this.previousButton.disable(); - } - - if (this.tipStore.indexOf(record) === this.tipStore.getTotalCount() - 1) { - this.nextButton.disable(); - } else { - this.nextButton.enable(); - } - } else { - if (this.tipStore.indexOf(record) > this.getFirstUnreadTip()) { - this.previousButton.enable(); - } else { - this.previousButton.disable(); - } - - - if (this.tipStore.indexOf(record) >= this.getLastUnreadTip()) { - this.nextButton.disable(); - } else { - this.nextButton.enable(); - } - } - - }, - /** - * Returns the index of the first unread tip, or null if there's no unread tip. - * @returns int The index of the first unread tip, or null - */ - getFirstUnreadTip: function () { - for (var i=0;i<this.tipStore.getTotalCount();i++) { - if (this.tipStore.getAt(i).get("read") === false) { - return i; - } - } - - return null; - }, - /** - * Returns the index of the last unread tip, or null if there's no unread tip. - * @returns int The index of the last unread tip, or null - */ - getLastUnreadTip: function () { - for (var i=this.tipStore.getTotalCount()-1;i>-1;i--) { - if (this.tipStore.getAt(i).get("read") === false) { - return i; - } - } - - return null; - }, - /** - * Marks the current tip as read. Commits the information to the server. - */ - markTipRead: function () { - this.currentTip.set("read", true); - this.currentTip.commit(); - - var call = new PartKeepr.ServiceCall("TipOfTheDay", "markTipAsRead"); - call.setLoadMessage(sprintf(i18n("Marking tip %s as read..."), this.currentTip.get("name"))); - call.setParameter("name", this.currentTip.get("name")); - call.doCall(); - }, - /** - * Displays the next tip - */ - displayNextTip: function () { - this.retrieveTip("ASC"); - }, - /** - * Displays the previous tip - */ - displayPreviousTip: function () { - this.retrieveTip("DESC"); - }, - /** - * Displays the next or previous tip. - * - * @param dir string Either "ASC" or "DESC", which denotes the direction to search for the next tip - */ - retrieveTip: function (dir) { - var startIdx = -1, record = null; - - if (this.currentTip) { - startIdx = this.tipStore.indexOf(this.currentTip); - } - - if (dir === "ASC") { - record = this.extractNextTip(startIdx); - } else { - record = this.extractPreviousTip(startIdx); - } - - if (record) { - this.currentTip = record; - this.displayTip(record); - } - }, - /** - * Returns the record with the next tip - * @param startIdx The index to start searching from - * @returns record The record with the next tip - */ - extractNextTip: function (startIdx) { - var record = null, foundRecord = null; - if (this.displayReadTipsCheckbox.getValue() === true) { - var tmpIdx = startIdx + 1; - if (tmpIdx > this.tipStore.getTotalCount() - 1) { - tmpIdx = this.tipStore.getTotalCount() - 1; - } - - foundRecord = this.tipStore.getAt(tmpIdx); - } else { - for (var i = startIdx+1; i < this.tipStore.getTotalCount();i++) { - record = this.tipStore.getAt(i); - if (record.get("read") === false) { - foundRecord = record; - break; - } - } - } - - return foundRecord; - }, - /** - * Returns the record with the previous tip - * @param startIdx The index to start searching from - * @returns record The record with the previous tip - */ - extractPreviousTip: function (startIdx) { - var record = null, foundRecord = null; - if (this.displayReadTipsCheckbox.getValue() === true) { - var tmpIdx = startIdx - 1; - if (tmpIdx < 0) { - tmpIdx = 0; - } - - foundRecord = this.tipStore.getAt(tmpIdx); - } else { - for (var i = startIdx - 1; i > -1;i--) { - record = this.tipStore.getAt(i); - - if (record.get("read") === false) { - foundRecord = record; - break; - } - } - } - - - return foundRecord; - } - - -});- \ No newline at end of file + extend: 'Ext.window.Window', + + /* Defines the title template. */ + titleTemplate: i18n("Tip of the Day"), + + /* Cosmetic settings */ + width: 600, + height: 300, + + minWidth: 600, + minHeight: 300, + + layout: 'fit', + + /** + * Stores the currently displayed tip, or null if none is displayed + * @var Ext.data.Record + */ + currentTip: null, + + /** + * Holds an instance of the TipOfTheDay store. + */ + tipStore: null, + + /** + * Holds an instance of the TipOfTheDayHistory store + */ + tipHistoryStore: null, + + /** + * Initializes the window. Adds the iframe used for displaying tips, as well + * as the user controls (prev/next buttons, config checkboxes). + */ + initComponent: function () + { + // Initialize the window with the title template + this.title = this.titleTemplate; + + // Set the tip store + this.tipStore = Ext.data.StoreManager.lookup('TipOfTheDayStore'); + this.tipHistoryStore = Ext.data.StoreManager.lookup('TipOfTheDayHistoryStore'); + + // Set the tip display iframe and add it to the items + this.tipDisplay = Ext.create("Ext.ux.SimpleIFrame", { + border: false + }); + + this.items = this.tipDisplay; + + // Initialize previous and next buttons + this.previousButton = Ext.create("Ext.button.Button", { + text: i18n("Previous Tip"), + handler: Ext.bind(this.displayPreviousTip, this), + icon: 'bundles/partkeeprfrontend/images/icons/tip_previous.png', + disabled: true + }); + + this.nextButton = Ext.create("Ext.button.Button", { + text: i18n("Next Tip"), + icon: 'bundles/partkeeprfrontend/images/icons/tip_next.png', + handler: Ext.bind(this.displayNextTip, this) + }); + + // Initializes the "show tips on login" checkbox as well as the "show read tips" checkbox + this.showTipsCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Show Tips on login"), + handler: Ext.bind(this.showTipsHandler, this) + }); + + this.displayReadTipsCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Show read tips"), + handler: Ext.bind(this.showReadTipsHandler, this) + }); + + // Initialize the "show tips" checkbox with the user preference + if (PartKeepr.getApplication().getUserPreference("partkeepr.tipoftheday.showtips") === false) { + this.showTipsCheckbox.setValue(false); + } else { + this.showTipsCheckbox.setValue(true); + } + + // Append the controls to the bottom toolbar + this.dockedItems = [ + { + xtype: 'toolbar', + dock: 'bottom', + ui: 'footer', + defaults: {minWidth: 100}, + pack: 'start', + items: [ + this.previousButton, + this.nextButton, + '->', + this.showTipsCheckbox, + this.displayReadTipsCheckbox + ] + } + ]; + + // Auto-load the next unread tip on window display + this.on("show", this.displayNextTip, this); + + // Window destroy handler + this.on("destroy", this.onDestroy, this); + this.callParent(); + }, + /** + * If the "show read tips" checkbox was clicked, update the buttons + * to reflect the tip navigation. + */ + showReadTipsHandler: function () + { + this.updateButtons(this.currentTip); + }, + /** + * Destroy handler. Removes the "read tip" timer. + */ + onDestroy: function () + { + this.cancelReadTimer(); + }, + /** + * Cancels the read timer. + */ + cancelReadTimer: function () + { + if (this.markAsReadTask) { + this.markAsReadTask.cancel(); + } + }, + /** + * Handler when the "show tips" checkbox was clicked. + */ + showTipsHandler: function (checkbox, checked) + { + PartKeepr.getApplication().setUserPreference("partkeepr.tipoftheday.showtips", checked); + }, + /** + * Displays a specific tip of the day. + * @param record The record which contains the information regarding the tip + */ + displayTip: function (record) + { + // Cancel the old read timer + this.cancelReadTimer(); + + // Update buttons to reflect position + this.updateButtons(record); + + // Set the title to the tip name + this.setTitle(this.titleTemplate + ": " + record.get("name")); + + // Set iframe to the tip url + this.tipDisplay.setSrc(record.get("url")); + + // Fire up delayed task to mark the tip as read + this.markAsReadTask = new Ext.util.DelayedTask(this.markTipRead, this); + this.markAsReadTask.delay(5000); + + }, + /** + * Updates the navigation buttons. + * + * This method has two modes, depending on which state the "show read tips" checkbox is in. + * @param record The currently displayed tip + */ + updateButtons: function (record) + { + if (this.displayReadTipsCheckbox.getValue() === true) { + if (this.tipStore.indexOf(record) > 0) { + this.previousButton.enable(); + } else { + this.previousButton.disable(); + } + + if (this.tipStore.indexOf(record) === this.tipStore.getTotalCount() - 1) { + this.nextButton.disable(); + } else { + this.nextButton.enable(); + } + } else { + if (this.tipStore.indexOf(record) > this.getFirstUnreadTip()) { + this.previousButton.enable(); + } else { + this.previousButton.disable(); + } + + + if (this.tipStore.indexOf(record) >= this.getLastUnreadTip()) { + this.nextButton.disable(); + } else { + this.nextButton.enable(); + } + } + + }, + /** + * Returns the index of the first unread tip, or null if there's no unread tip. + * @returns int The index of the first unread tip, or null + */ + getFirstUnreadTip: function () + { + for (var i = 0; i < this.tipStore.getTotalCount(); i++) { + if (this.tipStore.getAt(i).get("read") === false) { + return i; + } + } + + return null; + }, + /** + * Returns the index of the last unread tip, or null if there's no unread tip. + * @returns int The index of the last unread tip, or null + */ + getLastUnreadTip: function () + { + for (var i = this.tipStore.getTotalCount() - 1; i > -1; i--) { + if (this.tipStore.getAt(i).get("read") === false) { + return i; + } + } + + return null; + }, + isTipRead: function ( + tip + ) + { + /*var filter = Ext.create("Ext.util.Filter", { + property: + }), + this.tipHistoryStore.*/ + + }, + /** + * Marks the current tip as read. Commits the information to the server. + */ + markTipRead: function () + { + this.currentTip.set("read", true); + this.currentTip.commit(); + + var call = new PartKeepr.ServiceCall("TipOfTheDay", "markTipAsRead"); + call.setLoadMessage(sprintf(i18n("Marking tip %s as read..."), this.currentTip.get("name"))); + call.setParameter("name", this.currentTip.get("name")); + call.doCall(); + }, + /** + * Displays the next tip + */ + displayNextTip: function () + { + this.retrieveTip("ASC"); + }, + /** + * Displays the previous tip + */ + displayPreviousTip: function () + { + this.retrieveTip("DESC"); + }, + /** + * Displays the next or previous tip. + * + * @param dir string Either "ASC" or "DESC", which denotes the direction to search for the next tip + */ + retrieveTip: function (dir) + { + var startIdx = -1, record = null; + + if (this.currentTip) { + startIdx = this.tipStore.indexOf(this.currentTip); + } + + if (dir === "ASC") { + record = this.extractNextTip(startIdx); + } else { + record = this.extractPreviousTip(startIdx); + } + + if (record) { + this.currentTip = record; + this.displayTip(record); + } + }, + /** + * Returns the record with the next tip + * @param startIdx The index to start searching from + * @returns record The record with the next tip + */ + extractNextTip: function (startIdx) + { + var record = null, foundRecord = null; + if (this.displayReadTipsCheckbox.getValue() === true) { + var tmpIdx = startIdx + 1; + if (tmpIdx > this.tipStore.getTotalCount() - 1) { + tmpIdx = this.tipStore.getTotalCount() - 1; + } + + foundRecord = this.tipStore.getAt(tmpIdx); + } else { + for (var i = startIdx + 1; i < this.tipStore.getTotalCount(); i++) { + record = this.tipStore.getAt(i); + if (record.get("read") === false) { + foundRecord = record; + break; + } + } + } + + return foundRecord; + }, + /** + * Returns the record with the previous tip + * @param startIdx The index to start searching from + * @returns record The record with the previous tip + */ + extractPreviousTip: function (startIdx) + { + var record = null, foundRecord = null; + if (this.displayReadTipsCheckbox.getValue() === true) { + var tmpIdx = startIdx - 1; + if (tmpIdx < 0) { + tmpIdx = 0; + } + + foundRecord = this.tipStore.getAt(tmpIdx); + } else { + for (var i = startIdx - 1; i > -1; i--) { + record = this.tipStore.getAt(i); + + if (record.get("read") === false) { + foundRecord = record; + break; + } + } + } + + + return foundRecord; + } + + +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js @@ -13,11 +13,47 @@ Ext.define("PartKeepr.data.HydraModel", { * @param {Function} callback (optional) A callback function, or null if not required * @param {boolean} reload (optional) Triggers a reload of the model after executing the action */ - callAction: function (action, parameters, callback, reload) + callPutAction: function (action, parameters, callback, reload) { var proxy = this.getProxy(); - proxy.callAction(this, action, parameters, callback, reload); + proxy.callAction(this, action, "PUT", parameters, callback, reload); + }, + /** + * Calls an action relative to the entity. + * + * For example, if the entity has a method called "setDefault" and your ID is + * "/PartKeepr/web/app_dev.php/api/part_measurement_units/1", callAction would call + * "/PartKeepr/web/app_dev.php/api/part_measurement_units/1/setDefault" as a result. + * + * @param {String} action The action name + * @param {Object} parameters (optional) The parameters as JS object + * @param {Function} callback (optional) A callback function, or null if not required + * @param {boolean} reload (optional) Triggers a reload of the model after executing the action + */ + callGetAction: function (action, parameters, callback, reload) + { + var proxy = this.getProxy(); + + proxy.callAction(this, action, "GET", parameters, callback, reload); + }, + /** + * Calls an action relative to the entity. + * + * For example, if the entity has a method called "setDefault" and your ID is + * "/PartKeepr/web/app_dev.php/api/part_measurement_units/1", callAction would call + * "/PartKeepr/web/app_dev.php/api/part_measurement_units/1/setDefault" as a result. + * + * @param {String} action The action name + * @param {Object} parameters (optional) The parameters as JS object + * @param {Function} callback (optional) A callback function, or null if not required + * @param {boolean} reload (optional) Triggers a reload of the model after executing the action + */ + callDeleteAction: function (action, parameters, callback, reload) + { + var proxy = this.getProxy(); + + proxy.callAction(this, action, "DELETE", parameters, callback, reload); }, getData: function (options) { @@ -81,11 +117,17 @@ Ext.define("PartKeepr.data.HydraModel", { } }, inheritableStatics: { - callCollectionAction: function (action, parameters, callback) + callPostCollectionAction: function (action, parameters, callback, ignoreException) + { + var proxy = this.getProxy(); + + proxy.callCollectionAction(action, "POST", parameters, callback, ignoreException); + }, + callGetCollectionAction: function (action, parameters, callback, ignoreException) { var proxy = this.getProxy(); - proxy.callCollectionAction(action, parameters, callback); + proxy.callCollectionAction(action, "GET", parameters, callback, ignoreException); } } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js @@ -43,7 +43,9 @@ Ext.define("PartKeepr.data.HydraProxy", { { var headers = this.callParent(arguments); - headers["X-WSSE"] = PartKeepr.getApplication().getSessionManager().getWSSE(); + var provider = PartKeepr.Auth.AuthenticationProvider.getAuthenticationProvider(); + + Ext.apply(headers, provider.getHeaders()); return headers; }, @@ -88,12 +90,12 @@ Ext.define("PartKeepr.data.HydraProxy", { * * */ - callAction: function (record, action, parameters, callback, reload) + callAction: function (record, action, method, parameters, callback, reload) { var url = record.getId() + "/" + action; var request = Ext.create("Ext.data.Request"); - request.setMethod("PUT"); + request.setMethod(method); request.setUrl(url); if (Ext.isObject(parameters)) { request.setParams(parameters); @@ -122,12 +124,12 @@ Ext.define("PartKeepr.data.HydraProxy", { * * */ - callCollectionAction: function (action, parameters, callback) + callCollectionAction: function (action, method, parameters, callback, ignoreException) { var url = this.url + "/" + action; var request = Ext.create("Ext.data.Request"); - request.setMethod("PUT"); + request.setMethod(method); request.setUrl(url); if (Ext.isObject(parameters)) { request.setParams(parameters); @@ -137,7 +139,7 @@ Ext.define("PartKeepr.data.HydraProxy", { request.setCallback(function (options, success, response) { - this.processCallActionResponse(options, success, response); + this.processCallActionResponse(options, success, response, ignoreException); if (Ext.isFunction(callback)) { callback(options, success, response); @@ -146,13 +148,16 @@ Ext.define("PartKeepr.data.HydraProxy", { this.sendRequest(request); }, - processCallActionResponse: function (options, success, response) + processCallActionResponse: function (options, success, response, ignoreException) { - if (success !== false) { + if (success === true) { return; } - this.showException(response); + console.log(ignoreException); + if (!ignoreException) { + this.showException(response); + } }, showException: function (response) { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/TipOfTheDayHistoryStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/TipOfTheDayHistoryStore.js @@ -0,0 +1,20 @@ +Ext.define('PartKeepr.data.store.TipOfTheDayHistoryStore', { + extend: 'Ext.data.Store', + + /** + * The store ID to use + */ + storeId: 'TipOfTheDayHistoryStore', + + /** + * Automatically load the store + */ + autoLoad: true, + + /** + * The model to use + */ + model: "PartKeepr.TipOfTheDayBundle.Entity.TipOfTheDayHistory", + + pageSize: 99999999 +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/TipOfTheDayStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/TipOfTheDayStore.js @@ -0,0 +1,20 @@ +Ext.define('PartKeepr.data.store.TipOfTheDayStore', { + extend: 'Ext.data.Store', + + /** + * The store ID to use + */ + storeId: 'TipOfTheDayStore', + + /** + * Automatically load the store + */ + autoLoad: true, + + /** + * The model to use + */ + model: "PartKeepr.TipOfTheDayBundle.Entity.TipOfTheDay", + + pageSize: 99999999 +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js b/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js @@ -4,6 +4,8 @@ PartKeepr.application = null; Ext.application({ name: 'PartKeepr', + loginManager: null, + launch: function () { Ext.setGlyphFontFamily('FontAwesome'); @@ -18,27 +20,31 @@ Ext.application({ PartKeepr.setMaxUploadSize(window.parameters.maxUploadSize); PartKeepr.setAvailableImageFormats(window.parameters.availableImageFormats); + var authenticationProvider = Ext.create(window.parameters.authentication_provider); + PartKeepr.Auth.AuthenticationProvider.setAuthenticationProvider(authenticationProvider); + this.sessionManager = new PartKeepr.SessionManager(); - /* Automatic session starting is active. This disables login/logout functionality. */ - if (window.parameters.auto_start_session) { - this.getSessionManager().setSession(window.parameters.auto_start_session); - this.getStatusbar().connectionButton.hide(); - this.setUsername(window.parameters.autoLoginUsername); - this.onLogin(); - } else { - // If auto login is wanted (for e.g. demo systems), put it in here - this.sessionManager.on("login", this.onLogin, this); + var config = {}; - if (window.parameters.autoLoginUsername) { - this.sessionManager.login(window.parameters.autoLoginUsername, window.parameters.autoLoginPassword); - } else { - this.sessionManager.login(); - } + if (window.parameters.autoLoginUsername) { + config.autoLogin = true; + config.autoLoginUsername = window.parameters.autoLoginUsername; + config.autoLoginPassword = window.parameters.autoLoginPassword; } + this.loginManager = Ext.create("PartKeepr.Auth.LoginManager", config); + this.loginManager.on("login", this.onLogin, this); + this.loginManager.on("logout", this.onLogout, this); + this.loginManager.login(); + + Ext.fly(document.body).on('contextmenu', this.onContextMenu, this); }, + getLoginManager: function () + { + return this.loginManager; + }, getPartManager: function () { return this.partManager; @@ -84,11 +90,15 @@ Ext.application({ this.displayMOTD(); } - this.setSession(this.getSessionManager().getSession()); - - this.getStatusbar().getConnectionButton().setConnected(); + this.getStatusbar().setConnected(); }, + onLogout: function () + { + this.menuBar.disable(); + this.centerPanel.removeAll(true); + this.getStatusbar().setDisconnected(); + }, /** * Re-creates the part manager. This is usually called when the "compactLayout" configuration option has been * changed. @@ -133,12 +143,13 @@ Ext.application({ */ displayTipOfTheDayWindow: function () { - if (!this.tipOfTheDayStore._loaded) { + if (!Ext.data.StoreManager.lookup('TipOfTheDayStore') || !Ext.data.StoreManager.lookup( + 'TipOfTheDayStore').isLoaded()) { this.displayTipWindowTask.delay(100); return; } - if (PartKeepr.getApplication().getUserPreference("partkeepr.tipoftheday.showtips") !== false) { + if (PartKeepr.getApplication().getUserPreference("partkeepr.tipoftheday.showtips") !== "false") { var j = Ext.create("PartKeepr.TipOfTheDayWindow"); if (j.getLastUnreadTip() !== null) { @@ -219,12 +230,6 @@ Ext.application({ Ext.defer(this.doUnacknowledgedNoticesCheck, 10000, this); }, - logout: function () - { - this.menuBar.disable(); - this.centerPanel.removeAll(true); - this.getSessionManager().logout(); - }, createGlobalStores: function () { this.footprintStore = Ext.create("Ext.data.Store", @@ -276,21 +281,14 @@ Ext.application({ autoLoad: true }); - this.tipOfTheDayStore = Ext.create("Ext.data.Store", - { - model: 'PartKeepr.TipOfTheDayBundle.Entity.TipOfTheDay', - pageSize: 99999999, - autoLoad: true, - listeners: { - scope: this, - load: this.storeLoaded - } - }); - this.userPreferenceStore = Ext.create("PartKeepr.data.store.UserPreferenceStore", { model: 'PartKeepr.AuthBundle.Entity.UserPreference', }); + + this.tipOfTheDayStore = Ext.create("PartKeepr.data.store.TipOfTheDayStore"); + this.tipOfTheDayHistoryStore = Ext.create("PartKeepr.data.store.TipOfTheDayHistoryStore"); + }, storeLoaded: function (store) { @@ -304,10 +302,6 @@ Ext.application({ { return this.admin; }, - getTipOfTheDayStore: function () - { - return this.tipOfTheDayStore; - }, /** * Queries for a specific user preference. Returns either the value or a default value if * the preference was not found. @@ -625,8 +619,8 @@ PartKeepr.getMaxUploadSize = function () PartKeepr.bytesToSize = function (bytes) { var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) { - return 'n/a'; + if (bytes === 0) { + return 'n/a'; } var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Util/ServiceCall.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Util/ServiceCall.js @@ -68,7 +68,9 @@ Ext.define('PartKeepr.ServiceCall', { }; if (!this.anonymous) { - headers["X-WSSE"] = PartKeepr.getApplication().getSessionManager().getWSSE(); + var provider = PartKeepr.Auth.AuthenticationProvider.getAuthenticationProvider(); + + Ext.apply(headers, provider.getHeaders()); } Ext.Ajax.request({ diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -51,7 +51,13 @@ {% endfor %} {% javascripts output='js/compiled/main2.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Auth/LoginManager.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Bugfixes/Ext.grid.feature.Summary-selectorFix.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Auth/AuthenticationProvider.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Auth/HTTPBasicAuthenticationProvider.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Auth/WSSEAuthenticationProvider.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/TipOfTheDayStore.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/TipOfTheDayHistoryStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/ProjectReport.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/ProjectReportList.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/SystemInformationRecord.js' diff --git a/src/PartKeepr/TipOfTheDayBundle/Entity/TipOfTheDay.php b/src/PartKeepr/TipOfTheDayBundle/Entity/TipOfTheDay.php @@ -6,6 +6,7 @@ use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; use PartKeepr\PartKeepr; use PartKeepr\Util\BaseEntity; use PartKeepr\Util\Configuration; +use Symfony\Component\Serializer\Annotation\Groups; /** * Represents a tip of the day. @@ -23,6 +24,7 @@ class TipOfTheDay extends BaseEntity { /** * @ORM\Column(type="string") + * @Groups({"default"}) * @var string */ private $name; @@ -115,4 +117,4 @@ class TipOfTheDay extends BaseEntity return $aPageNames; } -}- \ No newline at end of file +} diff --git a/src/PartKeepr/TipOfTheDayBundle/Entity/TipOfTheDayHistory.php b/src/PartKeepr/TipOfTheDayBundle/Entity/TipOfTheDayHistory.php @@ -3,7 +3,9 @@ namespace PartKeepr\TipOfTheDayBundle\Entity; use Doctrine\ORM\Mapping as ORM; use PartKeepr\AuthBundle\Entity\User; +use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; use PartKeepr\Util\BaseEntity; +use Symfony\Component\Serializer\Annotation\Groups; /** * Represents a tip of the day history entry. @@ -11,11 +13,13 @@ use PartKeepr\Util\BaseEntity; * This entity stores each tip of the day the user has already seen. * * @ORM\Entity + * @TargetService(uri="/api/tip_of_the_day_histories") **/ class TipOfTheDayHistory extends BaseEntity { /** * @ORM\Column(type="string") + * @Groups({"default"}) * @var string */ private $name; @@ -23,6 +27,7 @@ class TipOfTheDayHistory extends BaseEntity /** * Defines the user * @ORM\ManyToOne(targetEntity="PartKeepr\AuthBundle\Entity\User") + * @Groups({"default"}) * * @var \PartKeepr\AuthBundle\Entity\User */ @@ -39,6 +44,14 @@ class TipOfTheDayHistory extends BaseEntity } /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** * Sets the tip of the day name the user already has seen * * @param string $name The tip name @@ -48,4 +61,11 @@ class TipOfTheDayHistory extends BaseEntity $this->name = $name; } -}- \ No newline at end of file + /** + * @return string + */ + public function getName() + { + return $this->name; + } +}