From d3f5efb2c43b85cc94b10e85266bca44f2002b84 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Fri, 7 Jul 2017 21:08:04 +0200 Subject: [PATCH 01/23] WIP: Private pusher channel [skip ci] --- client/src/elm/App/Model.elm | 4 + client/src/elm/App/PageType.elm | 2 +- client/src/elm/App/Update.elm | 12 +- client/src/elm/Pusher/Model.elm | 61 +++++++++ client/src/elm/Pusher/Update.elm | 120 ++++++++++++++++++ client/src/js/README.md | 2 +- client/src/js/app.js | 84 +++++++----- .../custom/hedley_item/hedley_item.module | 15 ++- .../custom/hedley_pusher/hedley_pusher.module | 2 +- 9 files changed, 264 insertions(+), 38 deletions(-) create mode 100644 client/src/elm/Pusher/Update.elm diff --git a/client/src/elm/App/Model.elm b/client/src/elm/App/Model.elm index c7dcd56..f2c6bf1 100644 --- a/client/src/elm/App/Model.elm +++ b/client/src/elm/App/Model.elm @@ -11,6 +11,7 @@ import App.PageType exposing (Page(..)) import Config.Model import Date exposing (Date) import Pages.Login.Model exposing (emptyModel, Model) +import Pusher.Model import RemoteData exposing (RemoteData(..), WebData) import ItemManager.Model exposing (emptyModel, Model) import Time exposing (Time) @@ -21,6 +22,7 @@ type Msg = HandleOfflineEvent (Result String Bool) | Logout | MsgItemManager ItemManager.Model.Msg + | MsgPusher Pusher.Model.Msg | PageLogin Pages.Login.Model.Msg | SetActivePage Page | SetCurrentDate Date @@ -36,6 +38,7 @@ type alias Model = , offline : Bool , pageLogin : Pages.Login.Model.Model , pageItem : ItemManager.Model.Model + , pusher : Pusher.Model.Model , sidebarOpen : Bool , user : WebData User } @@ -61,6 +64,7 @@ emptyModel = , offline = False , pageLogin = Pages.Login.Model.emptyModel , pageItem = ItemManager.Model.emptyModel + , pusher = Pusher.Model.emptyModel , sidebarOpen = False , user = NotAsked } diff --git a/client/src/elm/App/PageType.elm b/client/src/elm/App/PageType.elm index c4de642..b0cc704 100644 --- a/client/src/elm/App/PageType.elm +++ b/client/src/elm/App/PageType.elm @@ -1,6 +1,6 @@ module App.PageType exposing (Page(..)) -{-| Prevent circula dependency. +{-| Prevent circular dependency. -} diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index 5a086ae..cdc04c2 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -5,13 +5,13 @@ import App.PageType exposing (Page(..)) import Config import Date import Dict -import Http import ItemManager.Model import ItemManager.Update import Json.Decode exposing (bool, decodeValue) import Json.Encode exposing (Value) import Pages.Login.Update import Pusher.Model +import Pusher.Update import Pusher.Utils exposing (getClusterName) import RemoteData exposing (RemoteData(..), WebData) import Task @@ -124,6 +124,15 @@ update msg model = -- If we don't have a user, we have nothing to do. model ! [] + MsgPusher subMsg -> + let + ( val, cmd ) = + Pusher.Update.update backendUrl subMsg model.pusher + in + ( { model | pusher = val } + , Cmd.map MsgPusher cmd + ) + PageLogin msg -> let ( val, cmds, ( webDataUser, accessToken ) ) = @@ -240,6 +249,7 @@ subscriptions model = [ Sub.map MsgItemManager <| ItemManager.Update.subscriptions model.pageItem model.activePage , Time.every minute Tick , offline (decodeValue bool >> HandleOfflineEvent) + , Sub.map MsgPusher <| Pusher.Update.subscription ] diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index 39b65d3..1d05366 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -3,6 +3,19 @@ module Pusher.Model exposing (..) import Item.Model exposing (Item, ItemId) +type alias Model = + { connectionStatus : ConnectionStatus + , errors : List PusherError + , showErrorModal : Bool + } + +emptyModel : Model +emptyModel = + { connectionStatus = Initialized + , errors = [] + , showErrorModal = False + } + type Cluster = ApSouthEast1 | EuWest1 @@ -25,8 +38,56 @@ type PusherEventData = ItemUpdate Item +type PusherKey + = PusherKey String + + +type AccessToken + = AccessToken String + + +type alias PusherConfig = + { key : String + , authEndpoint : String + } + + +{-| Represents the state of our pusher connection. +This mostly tracks +The `(Maybe Int)` parameters track when the next reconnection attempt will take +place, if that is known. +We'll start in `Initialized` state, and stay there until we get a `Login` +message. At that point, we'll gradually proceed through `Connecting` to +`Connected` (if all goes well). +-} +type ConnectionStatus + = Initialized + | Connecting (Maybe Int) + | Connected + | Unavailable (Maybe Int) + | Failed + | Disconnected + | Other String + + +type alias PusherError = + { code : Int + , message : String + } + + {-| Return the event names that should be added via JS. -} eventNames : List String eventNames = [ "item__update" ] + + +type Msg + = HandleError PusherError + | HandleStateChange String + | HandleConnectingIn Int + | ShowErrorModal + | HideErrorModal + | Login PusherKey AccessToken + | Logout diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm new file mode 100644 index 0000000..1fe2bc7 --- /dev/null +++ b/client/src/elm/Pusher/Update.elm @@ -0,0 +1,120 @@ +port module Pusher.Update exposing (update, subscription) + +import Config.Model exposing (BackendUrl) +import Pusher.Model exposing (..) + + +{-| Login to pusher. +-} +port pusherLogin : PusherConfig -> Cmd msg + + +{-| Logout from pusher. +-} +port pusherLogout : () -> Cmd msg + + +{-| Receive pusher errors. +-} +port pusherError : (PusherError -> msg) -> Sub msg + + +{-| Receive pusher state changes. +-} +port pusherState : (String -> msg) -> Sub msg + + +{-| Receive notice of upcoming connection attempts. +-} +port pusherConnectingIn : (Int -> msg) -> Sub msg + + +{-| Subscription to connection status. +-} +subscription : Sub Msg +subscription = + Sub.batch + [ pusherError HandleError + , pusherState HandleStateChange + , pusherConnectingIn HandleConnectingIn + ] + + +update : BackendUrl -> Msg -> Model -> ( Model, Cmd Msg ) +update backendUrl msg model = + case msg of + HandleConnectingIn delay -> + let + connectionStatus = + case model.connectionStatus of + Connecting _ -> + Connecting (Just delay) + + Unavailable _ -> + Unavailable (Just delay) + + _ -> + model.connectionStatus + in + ( { model | connectionStatus = connectionStatus } + , Cmd.none + ) + + HandleError error -> + -- We could consider keeping only X number of errors + ( { model | errors = error :: model.errors } + , Cmd.none + ) + + HandleStateChange state -> + let + connectionStatus = + case state of + "intialized" -> + Initialized + + "connecting" -> + Connecting Nothing + + "connected" -> + Connected + + "unavailable" -> + Unavailable Nothing + + "failed" -> + Failed + + "disconnected" -> + Disconnected + + _ -> + Other state + in + ( { model | connectionStatus = connectionStatus } + , Cmd.none + ) + + ShowErrorModal -> + ( { model | showErrorModal = True } + , Cmd.none + ) + + HideErrorModal -> + ( { model | showErrorModal = False } + , Cmd.none + ) + + Login (PusherKey pusherKey) (AccessToken accessToken) -> + let + pusherConfig = + { key = pusherKey + , authEndpoint = backendUrl ++ "/api/pusher_auth?access_token=" ++ accessToken + } + in + ( model + , pusherLogin pusherConfig + ) + + Logout -> + ( model, pusherLogout () ) diff --git a/client/src/js/README.md b/client/src/js/README.md index b9dfb1e..ca2cc10 100644 --- a/client/src/js/README.md +++ b/client/src/js/README.md @@ -1 +1 @@ -Place JS, and JS-inerop files in this folder. +Place JS, and JS-interop files in this folder. diff --git a/client/src/js/app.js b/client/src/js/app.js index 368fe1c..8a30c8b 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -1,43 +1,69 @@ var elmApp = Elm.Main.fullscreen({ - accessToken: localStorage.getItem('accessToken') || '', - hostname: window.location.hostname + accessToken: localStorage.getItem('accessToken') || '', + hostname: window.location.hostname }); elmApp.ports.accessTokenPort.subscribe(function(accessToken) { - localStorage.setItem('accessToken', accessToken); + localStorage.setItem('accessToken', accessToken); }); +var pusher = null; -elmApp.ports.pusherKey.subscribe(function(appKey) { - var pusher = new Pusher(appKey[0], { - cluster: appKey[1] +var pusherConfig = null; + +elmApp.ports.pusherLogout.subscribe(function () { + if (pusher) { + pusher.disconnect(); + pusher = null; + pusherConfig = null; + } +}); + +elmApp.ports.pusherLogin.subscribe(function(config) { + // If we had a previous connection, disconnect it. + if (pusher) { + pusher.disconnect(); + } + + pusherConfig = config; + + pusher = new Pusher(config.key, { + cluster: 'eu', + authEndpoint: config.authEndpoint + }); + + pusher.connection.bind('error', function (err) { + elmApp.ports.pusherError.send({ + message: err.data.message, + code: err.data.code, + when: Date.now() }); + }); + + pusher.connection.bind('state_change', function (states) { + elmApp.ports.pusherState.send(states.current); + }); - var channelName = 'general'; - - if (!pusher.channel(channelName)) { - var channel = pusher.subscribe(channelName); - - var eventNames = appKey[2]; - - eventNames.forEach(function(eventName) { - channel.bind(eventName, function(data) { - // We wrap the data with some information which will - // help us dispatch it on the Elm side - var event = { - eventType: eventName, - data: data - }; - elmApp.ports.pusherItemMessages.send(event); - }); - }); - } + pusher.connection.bind('connecting_in', function (delay) { + elmApp.ports.pusherConnectingIn.send(delay); + }); }); -Offline.on('down', function() { - elmApp.ports.offline.send(true); +elmApp.ports.subscribeToPrivateChannel.subscribe(function (params) { + if (!pusher) { + console.log ("Tried to subscribe to private pusher channel before login."); + return; + } + + var channelName = 'private-general'; + + if (!pusher.channel(channelName)) { + var channel = pusher.subscribe(channelName); + } }); -Offline.on('up', function() { - elmApp.ports.offline.send(false); +elmApp.ports.unsubscribeFromPrivateChannel.subscribe(function (sessionId) { + if (!pusher) return; + + pusher.unsubscribe('private-general'); }); diff --git a/server/hedley/modules/custom/hedley_item/hedley_item.module b/server/hedley/modules/custom/hedley_item/hedley_item.module index acb3050..fbd4c6b 100644 --- a/server/hedley/modules/custom/hedley_item/hedley_item.module +++ b/server/hedley/modules/custom/hedley_item/hedley_item.module @@ -11,6 +11,10 @@ include_once 'hedley_item.features.inc'; * Implements hook_node_insert(). */ function hedley_item_node_insert($node) { + if ($node->type != 'item') { + return; + } + hedley_item_trigger_pusher_on_item_data_change($node); } @@ -21,8 +25,9 @@ function hedley_item_node_update($node) { if ($node->type != 'item') { return; } + // Make sure we get updated data. - entity_get_controller('node')->resetCache(); + entity_get_controller('node')->resetCache([$node->nid]); hedley_item_trigger_pusher_on_item_data_change($node); } @@ -33,10 +38,6 @@ function hedley_item_node_update($node) { * A Item data node type. */ function hedley_item_trigger_pusher_on_item_data_change($node) { - if ($node->type != 'item') { - return; - } - if ($node->status == NODE_NOT_PUBLISHED) { // Node is not published. return; @@ -49,4 +50,8 @@ function hedley_item_trigger_pusher_on_item_data_change($node) { $data = $handler->get($node->nid); hedley_pusher_trigger_event('general', 'item__update', $data[0]); + + // Send dummy private event. + $data[0]['private_field'] = 'Here be secrets'; + hedley_pusher_trigger_event('private-general', 'item__update', $data[0]); } diff --git a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module index 103bb7d..effae54 100644 --- a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module +++ b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module @@ -45,7 +45,7 @@ function hedley_pusher_get_pusher() { $app_id = variable_get('hedley_pusher_app_id'); $app_cluster = variable_get('hedley_pusher_app_cluster'); - if (empty($app_key) || empty($app_secret) || empty($app_id) || empty($app_cluster)) { + if (!$app_key || !$app_secret || !$app_id || !$app_cluster) { throw new Exception('Pusher app is not configured properly.'); } From a1348fc2877fa8c7a814729703850f0a62194a65 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Fri, 7 Jul 2017 21:12:29 +0200 Subject: [PATCH 02/23] Re-adding offline interop [skip ci] --- client/src/js/app.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/js/app.js b/client/src/js/app.js index 8a30c8b..5d857cc 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -7,6 +7,14 @@ elmApp.ports.accessTokenPort.subscribe(function(accessToken) { localStorage.setItem('accessToken', accessToken); }); +Offline.on('down', function() { + elmApp.ports.offline.send(true); +}); + +Offline.on('up', function() { + elmApp.ports.offline.send(false); +}); + var pusher = null; var pusherConfig = null; @@ -51,7 +59,7 @@ elmApp.ports.pusherLogin.subscribe(function(config) { elmApp.ports.subscribeToPrivateChannel.subscribe(function (params) { if (!pusher) { - console.log ("Tried to subscribe to private pusher channel before login."); + console.log ('Tried to subscribe to private pusher channel before login.'); return; } From ee7995c62847206b7a99b40f6eb0c5b2dbaf1daa Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Fri, 7 Jul 2017 23:15:04 +0200 Subject: [PATCH 03/23] Pusher auth [skip ci] --- .../custom/hedley_pusher/hedley_pusher.info | 1 + .../custom/hedley_pusher/hedley_pusher.module | 34 +++-- .../hedley_pusher/tests/HedleyPusherAuth.test | 134 ++++++++++++++++++ .../HedleyRestfulPusherAuthResource.class.php | 78 ++++++++++ .../user/pusher_auth/1.0/pusher_auth__1_0.inc | 15 ++ 5 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test create mode 100644 server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php create mode 100644 server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc diff --git a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.info b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.info index a7aff3e..b26875b 100644 --- a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.info +++ b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.info @@ -3,3 +3,4 @@ core = 7.x description = "Hedley Pusher integration." package = Hedley dependencies[] = composer_manager +files[] = tests/HedleyPusherAuth.test diff --git a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module index effae54..62b9f96 100644 --- a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module +++ b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module @@ -8,22 +8,22 @@ /** * Trigger a Pusher event. * + * The event itself is fired after all the processing. + * * @param string $channel_name * The channel name. * @param string $event_name * The event name. * @param array $data * The data to transmit. - * - * @throws \PusherException - * @throws \RestfulServerConfigurationException - * - * @return bool - * The return value of the Pusher trigger action. */ function hedley_pusher_trigger_event($channel_name, $event_name, array $data = array()) { - $pusher = hedley_pusher_get_pusher(); - return $pusher->trigger($channel_name, $event_name, $data); + $queue = &drupal_static('hedley_pusher_static_queue', []); + $queue[] = [ + 'channel' => $channel_name, + 'name' => $event_name, + 'data' => $data, + ]; } /** @@ -56,3 +56,21 @@ function hedley_pusher_get_pusher() { return new Pusher($app_key, $app_secret, $app_id, $options); } + +/** + * Implements hook_exit(). + * + * Sends out Pusher messages in a batch. + */ +function hedley_pusher_exit() { + $queue = &drupal_static('hedley_pusher_static_queue', []); + if (!empty($queue)) { + try { + $pusher = hedley_pusher_get_pusher(); + $pusher->triggerBatch($queue); + } + catch (Exception $e) { + watchdog('hedley_pusher', 'Could not transmit Pusher events: !queue', ['!queue' => serialize($queue)]); + } + } +} diff --git a/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test b/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test new file mode 100644 index 0000000..5974c11 --- /dev/null +++ b/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test @@ -0,0 +1,134 @@ + 'HedleyPusherAuth tests', + 'description' => 'Tests the pusher integration.', + 'group' => 'Hedley', + ]; + } + + /** + * Initialize; Creates a dummy bid. + */ + public function setUp() { + parent::setUp(); + + variable_set('hedley_pusher_app_key', 'key'); + variable_set('hedley_pusher_app_secret', 'secret'); + variable_set('hedley_pusher_app_id', 'id'); + variable_set('hedley_pusher_app_cluster', 'cluster'); + + $this->handler = restful_get_restful_handler('pusher_auth'); + + $values = [ + 'type' => 'item', + 'title' => 'Test item', + 'uid' => $this->drupalCreateUser()->uid, + 'status' => NODE_PUBLISHED, + ]; + + $node = entity_create('node', $values); + node_save($node); + $this->itemNid = $node->nid; + } + + /** + * Test pusher auth token restful endpoint. + */ + public function testPusherAuth() { + // Arguments for logging into a private sale channel. + $pusher_auth_arguments = [ + 'socket_id' => '123.123', + 'channel_name' => 'private-general', + ]; + + // Try to auth pusher with an unprivileged user. + $this->handler->setAccount($this->drupalCreateUser()); + try { + $this->handler->post('', $pusher_auth_arguments); + $this->fail('Posting to the pusher auth endpoint with an unprivileged user should have thrown an exception.'); + } + catch (\RestfulForbiddenException $e) { + $this->assertTrue(strstr($e->getMessage(), 'no "access private channel" permission'), "The exception was thrown because the user don't have the needed permission."); + } + + // Auth with an admin, and check that a token is retrieved. + $this->handler->setAccount(user_load(1)); + $result = $this->handler->post('', $pusher_auth_arguments); + $this->assertTrue(intval($result['id']), 'Received auth token ID.'); + $this->assertTrue(preg_match('/^key:[\d|\w]+$/', $result['auth']), 'Received token in the expected format.'); + } + + /** + * Test that the private channel contains private data. + */ + public function testPrivatePusherData() { + $messages = self::getPusherMessages(TRUE, 'item__update'); + $this->assertTrue(is_array($messages) && $messages, 'Got at least one item__update message on the private channel.'); + foreach ($messages as $message) { + $this->assertTrue(!empty($message['data']['private_field']), '"private_field" is exposed on the private channel.'); + } + + $messages = self::getPusherMessages(FALSE, 'item__update'); + $this->assertTrue(is_array($messages) && $messages, 'Got at least one item__update message on the public channel.'); + foreach ($messages as $message) { + $this->assertTrue(!array_key_exists('private_field', $message['data']), '"private_field" is not exposed on the public channel.'); + } + } + + /** + * Get pusher messages of a certain type from the pusher queue. + * + * @param bool $privileged + * Whether to fetch messages from the privileged, or the public channels. + * @param string $name + * The pusher message name. + * + * @return array + * Array of pusher messages. + */ + protected static function getPusherMessages($privileged, $name) { + $channel = $privileged ? 'private-general' : 'general'; + // Get the entire pusher queue, and filter it by message name and channel. + $queue = &drupal_static('hedley_pusher_static_queue', []); + return array_filter($queue, function ($message) use ($channel, $name) { + return $message['name'] == $name && $message['channel'] == $channel; + }); + } + +} diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php new file mode 100644 index 0000000..b699df0 --- /dev/null +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php @@ -0,0 +1,78 @@ + [ + \RestfulInterface::POST => 'authenticatePusher', + ], + ]; + } + + /** + * Overrides \RestfulEntityBaseUser::publicFieldsInfo(). + */ + public function publicFieldsInfo() { + $public_fields = parent::publicFieldsInfo(); + + $public_fields['auth'] = array( + 'callback' => array($this, 'getPusherAuth'), + ); + + return $public_fields; + } + + /** + * Overrides \RestfulEntityBase::viewEntity(). + * + * Always return the current user. + */ + public function authenticatePusher() { + $request = $this->getRequest(); + + if (empty($request['socket_id'])) { + throw new \RestfulBadRequestException('"socket_id" property is missing'); + } + + // Verify the channel name. + if (empty($request['channel_name'])) { + throw new \RestfulBadRequestException('"channel_name" property is missing'); + } + if ($request['channel_name'] != 'private-general') { + throw new \RestfulBadRequestException(format_string('Unknown channel "@channel"', ['@channel' => $request['channel_name']])); + } + + $account = $this->getAccount(); + if (!user_access('access private channel', $account)) { + throw new \RestfulForbiddenException(format_string('User @name is trying to log into the privileged pusher channel "@channel", but has no "access private channel" permission.', ['@name' => $account->name, '@channel' => $request['channel_name']])); + } + + return $this->view($account->uid); + } + + /** + * Get the pusher auth. + */ + protected function getPusherAuth() { + $request = $this->getRequest(); + + $pusher = hedley_pusher_get_pusher(); + $result = $pusher->socket_auth($request['channel_name'], $request['socket_id']); + $data = drupal_json_decode($result); + + return $data['auth']; + } + +} diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc new file mode 100644 index 0000000..312827d --- /dev/null +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc @@ -0,0 +1,15 @@ + t('Pusher auth'), + 'description' => t('Authenticate user for the pusher privileged channel.'), + 'resource' => 'pusher_auth', + 'class' => 'HedleyRestfulPusherAuthResource', + 'entity_type' => 'user', + 'bundle' => 'user', + 'authentication_types' => TRUE, + 'formatter' => 'simple', +); From 0ade01932d02d9bbbebba33662c112007f01b812 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sat, 8 Jul 2017 19:33:01 +0200 Subject: [PATCH 04/23] Subscribing to the private channel [skip ci] --- client/src/elm/App/Update.elm | 18 ++++++++++ client/src/elm/Pages/Login/Decoder.elm | 2 +- client/src/elm/Pusher/Model.elm | 13 ++++--- client/src/elm/Pusher/Update.elm | 5 +-- client/src/js/app.js | 35 ++++++++++--------- .../HedleyRestfulFormatterSimple.class.php | 5 +-- 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index cdc04c2..51bee16 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -169,12 +169,30 @@ update msg model = _ -> modelUpdated ! [] + + pusherMsg = + case model.config of + Success config -> + -- TODO: Surely it's possible to clean up the + -- Login call. This part was written using this + -- method: http://2.bp.blogspot.com/-X6peGqFZFZ0/UcTqFQ2O21I/AAAAAAAAPi0/uRJyfIgg9uo/s1600/blindssuck.gif + (Task.succeed <| + MsgPusher <| + Pusher.Model.Login + (config.pusherKey) + (Pusher.Model.AccessToken accessToken) + ) + |> Task.perform identity + + _ -> + Cmd.none in ( modelWithRedirect , Cmd.batch [ Cmd.map PageLogin cmds , accessTokenPort accessToken , setActivePageCmds + , pusherMsg ] ) diff --git a/client/src/elm/Pages/Login/Decoder.elm b/client/src/elm/Pages/Login/Decoder.elm index 6044c21..37057df 100644 --- a/client/src/elm/Pages/Login/Decoder.elm +++ b/client/src/elm/Pages/Login/Decoder.elm @@ -9,7 +9,7 @@ import User.Model exposing (User) decodeUser : Decode.Decoder User decodeUser = - Decode.at [ "data", "0" ] <| UserDecoder.decodeUser + Decode.at [ "0" ] <| UserDecoder.decodeUser decodeAccessToken : Decode.Decoder AccessToken diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index 1d05366..8670b94 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -9,6 +9,7 @@ type alias Model = , showErrorModal : Bool } + emptyModel : Model emptyModel = { connectionStatus = Initialized @@ -16,6 +17,7 @@ emptyModel = , showErrorModal = False } + type Cluster = ApSouthEast1 | EuWest1 @@ -38,16 +40,13 @@ type PusherEventData = ItemUpdate Item -type PusherKey - = PusherKey String - - type AccessToken = AccessToken String type alias PusherConfig = { key : String + , cluster : String , authEndpoint : String } @@ -71,8 +70,8 @@ type ConnectionStatus type alias PusherError = - { code : Int - , message : String + { code : Maybe Int + , message : Maybe String } @@ -89,5 +88,5 @@ type Msg | HandleConnectingIn Int | ShowErrorModal | HideErrorModal - | Login PusherKey AccessToken + | Login PusherAppKey AccessToken | Logout diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm index 1fe2bc7..f3c477c 100644 --- a/client/src/elm/Pusher/Update.elm +++ b/client/src/elm/Pusher/Update.elm @@ -105,10 +105,11 @@ update backendUrl msg model = , Cmd.none ) - Login (PusherKey pusherKey) (AccessToken accessToken) -> + Login pusherAppKey (AccessToken accessToken) -> let pusherConfig = - { key = pusherKey + { key = pusherAppKey.key + , cluster = "eu" , authEndpoint = backendUrl ++ "/api/pusher_auth?access_token=" ++ accessToken } in diff --git a/client/src/js/app.js b/client/src/js/app.js index 5d857cc..570dbf7 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -36,15 +36,14 @@ elmApp.ports.pusherLogin.subscribe(function(config) { pusherConfig = config; pusher = new Pusher(config.key, { - cluster: 'eu', + cluster: config.cluster, authEndpoint: config.authEndpoint }); - pusher.connection.bind('error', function (err) { + pusher.connection.bind('error', function (error) { elmApp.ports.pusherError.send({ - message: err.data.message, - code: err.data.code, - when: Date.now() + message: error.error.data.message ? error.error.data.message : null, + code: error.error.code ? error.error.code : null }); }); @@ -55,23 +54,25 @@ elmApp.ports.pusherLogin.subscribe(function(config) { pusher.connection.bind('connecting_in', function (delay) { elmApp.ports.pusherConnectingIn.send(delay); }); -}); - -elmApp.ports.subscribeToPrivateChannel.subscribe(function (params) { - if (!pusher) { - console.log ('Tried to subscribe to private pusher channel before login.'); - return; - } var channelName = 'private-general'; if (!pusher.channel(channelName)) { var channel = pusher.subscribe(channelName); - } -}); -elmApp.ports.unsubscribeFromPrivateChannel.subscribe(function (sessionId) { - if (!pusher) return; + var eventNames = ['item__update']; + + eventNames.forEach(function(eventName) { + channel.bind(eventName, function(data) { + // We wrap the data with some information which will + // help us dispatch it on the Elm side + var event = { + eventType: eventName, + data: data + }; + elmApp.ports.pusherItemMessages.send(event); + }); + }); + } - pusher.unsubscribe('private-general'); }); diff --git a/server/hedley/modules/custom/hedley_restful/plugins/formatter/simple/HedleyRestfulFormatterSimple.class.php b/server/hedley/modules/custom/hedley_restful/plugins/formatter/simple/HedleyRestfulFormatterSimple.class.php index 2dab936..a3c6029 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/formatter/simple/HedleyRestfulFormatterSimple.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/formatter/simple/HedleyRestfulFormatterSimple.class.php @@ -25,12 +25,9 @@ public function prepare(array $data) { // 'application/problem+json; charset=utf-8'. if (!empty($data['status']) && floor($data['status'] / 100) != 2) { $this->contentType = 'application/problem+json; charset=utf-8'; - return $data; } - $output = ['data' => $data]; - - return $output; + return $data; } /** From 4f1c80a877d7509dc009724d6f95235a1d81cff7 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sat, 8 Jul 2017 21:03:29 +0200 Subject: [PATCH 05/23] Cleaning up me endpoint [skip ci] --- client/src/elm/Pages/Login/Decoder.elm | 7 ------- client/src/elm/Pages/Login/Update.elm | 1 + .../restful/user/me/1.0/HedleyRestfulMeResource.class.php | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/client/src/elm/Pages/Login/Decoder.elm b/client/src/elm/Pages/Login/Decoder.elm index 37057df..bb45840 100644 --- a/client/src/elm/Pages/Login/Decoder.elm +++ b/client/src/elm/Pages/Login/Decoder.elm @@ -3,13 +3,6 @@ module Pages.Login.Decoder exposing (..) import Base64 exposing (encode) import Json.Decode as Decode import Pages.Login.Model exposing (AccessToken) -import User.Decoder as UserDecoder exposing (decodeUser) -import User.Model exposing (User) - - -decodeUser : Decode.Decoder User -decodeUser = - Decode.at [ "0" ] <| UserDecoder.decodeUser decodeAccessToken : Decode.Decoder AccessToken diff --git a/client/src/elm/Pages/Login/Update.elm b/client/src/elm/Pages/Login/Update.elm index b69e090..8e694a5 100644 --- a/client/src/elm/Pages/Login/Update.elm +++ b/client/src/elm/Pages/Login/Update.elm @@ -7,6 +7,7 @@ import User.Model exposing (..) import Pages.Login.Model as Login exposing (..) import Pages.Login.Decoder exposing (..) import RemoteData exposing (RemoteData(..), WebData) +import User.Decoder exposing (decodeUser) import Utils.WebData exposing (sendWithHandler) diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php index 7e9fc3e..048445e 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php @@ -36,7 +36,7 @@ public function publicFieldsInfo() { */ public function viewEntity($entity_id) { $account = $this->getAccount(); - return array(parent::viewEntity($account->uid)); + return parent::viewEntity($account->uid); } } From b44ccbfbf0e1a6e2a28fef34b34e6417e7c07384 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sat, 8 Jul 2017 21:36:57 +0200 Subject: [PATCH 06/23] Fetching user's pusher channel from backend [skip ci] --- client/src/elm/App/Update.elm | 10 ++++++++- client/src/elm/Pusher/Model.elm | 1 + client/src/elm/Pusher/Update.elm | 5 +++-- client/src/elm/User/Decoder.elm | 1 + client/src/elm/User/Model.elm | 1 + client/src/js/app.js | 6 ++---- .../me/1.0/HedleyRestfulMeResource.class.php | 21 +++++++++++++++++++ .../HedleyRestfulPusherAuthResource.class.php | 4 ++-- 8 files changed, 40 insertions(+), 9 deletions(-) diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index 51bee16..419d583 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -126,8 +126,16 @@ update msg model = MsgPusher subMsg -> let + pusherChannel = + case model.user of + Success user -> + user.pusherChannel + + _ -> + "general" + ( val, cmd ) = - Pusher.Update.update backendUrl subMsg model.pusher + Pusher.Update.update backendUrl pusherChannel subMsg model.pusher in ( { model | pusher = val } , Cmd.map MsgPusher cmd diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index 8670b94..4f35134 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -48,6 +48,7 @@ type alias PusherConfig = { key : String , cluster : String , authEndpoint : String + , channel : String } diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm index f3c477c..37dadb2 100644 --- a/client/src/elm/Pusher/Update.elm +++ b/client/src/elm/Pusher/Update.elm @@ -40,8 +40,8 @@ subscription = ] -update : BackendUrl -> Msg -> Model -> ( Model, Cmd Msg ) -update backendUrl msg model = +update : BackendUrl -> String -> Msg -> Model -> ( Model, Cmd Msg ) +update backendUrl pusherChannel msg model = case msg of HandleConnectingIn delay -> let @@ -111,6 +111,7 @@ update backendUrl msg model = { key = pusherAppKey.key , cluster = "eu" , authEndpoint = backendUrl ++ "/api/pusher_auth?access_token=" ++ accessToken + , channel = pusherChannel } in ( model diff --git a/client/src/elm/User/Decoder.elm b/client/src/elm/User/Decoder.elm index 89715ee..04c504f 100644 --- a/client/src/elm/User/Decoder.elm +++ b/client/src/elm/User/Decoder.elm @@ -12,3 +12,4 @@ decodeUser = |> required "id" decodeInt |> required "label" string |> optional "avatar_url" string "https://github.com/foo.png?s=90" + |> required "pusher_channel" string diff --git a/client/src/elm/User/Model.elm b/client/src/elm/User/Model.elm index fb24f39..dd0fd3a 100644 --- a/client/src/elm/User/Model.elm +++ b/client/src/elm/User/Model.elm @@ -9,4 +9,5 @@ type alias User = { id : Int , name : String , avatarUrl : String + , pusherChannel : String } diff --git a/client/src/js/app.js b/client/src/js/app.js index 570dbf7..92fc176 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -55,10 +55,8 @@ elmApp.ports.pusherLogin.subscribe(function(config) { elmApp.ports.pusherConnectingIn.send(delay); }); - var channelName = 'private-general'; - - if (!pusher.channel(channelName)) { - var channel = pusher.subscribe(channelName); + if (!pusher.channel(config.channel)) { + var channel = pusher.subscribe(config.channel); var eventNames = ['item__update']; diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php index 048445e..55703a1 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php @@ -26,6 +26,14 @@ public function publicFieldsInfo() { $public_fields = parent::publicFieldsInfo(); unset($public_fields['self']); + + $public_fields['pusher_channel'] = array( + 'property' => 'uid', + 'process_callbacks' => [ + [$this, 'getPusherChannel'], + ], + ); + return $public_fields; } @@ -39,4 +47,17 @@ public function viewEntity($entity_id) { return parent::viewEntity($account->uid); } + /** + * Get pusher channel for user. + * + * @param int $uid + * User ID. + * + * @return string + * The pusher channel name: 'general' or 'private-general'. + */ + protected function getPusherChannel($uid) { + return user_access('access private pusher channel', user_load($uid)) ? 'private-general' : 'general'; + } + } diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php index b699df0..d8b96a6 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php @@ -55,8 +55,8 @@ public function authenticatePusher() { } $account = $this->getAccount(); - if (!user_access('access private channel', $account)) { - throw new \RestfulForbiddenException(format_string('User @name is trying to log into the privileged pusher channel "@channel", but has no "access private channel" permission.', ['@name' => $account->name, '@channel' => $request['channel_name']])); + if (!user_access('access private pusher channel', $account)) { + throw new \RestfulForbiddenException(format_string('User @name is trying to log into the privileged pusher channel "@channel", but has no "access private pusher channel" permission.', ['@name' => $account->name, '@channel' => $request['channel_name']])); } return $this->view($account->uid); From 5b62d1bb21a36bd7a9316aa27819ae9df3be7ac5 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sat, 8 Jul 2017 22:41:19 +0200 Subject: [PATCH 07/23] Logging into pusher only after user is fetched [skip ci] --- client/src/elm/App/Update.elm | 42 ++++++++++++++------------------ client/src/elm/Pusher/Model.elm | 1 + client/src/elm/Pusher/Update.elm | 4 ++- client/src/js/app.js | 6 +---- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index 419d583..24e5dce 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -12,7 +12,6 @@ import Json.Encode exposing (Value) import Pages.Login.Update import Pusher.Model import Pusher.Update -import Pusher.Utils exposing (getClusterName) import RemoteData exposing (RemoteData(..), WebData) import Task import Time exposing (minute) @@ -30,12 +29,7 @@ init flags = Just config -> let defaultCmds = - [ pusherKey - ( config.pusherKey.key - , getClusterName config.pusherKey.cluster - , Pusher.Model.eventNames - ) - , Task.perform SetCurrentDate Date.now + [ Task.perform SetCurrentDate Date.now ] ( cmds, activePage_ ) = @@ -179,18 +173,23 @@ update msg model = modelUpdated ! [] pusherMsg = - case model.config of - Success config -> - -- TODO: Surely it's possible to clean up the - -- Login call. This part was written using this - -- method: http://2.bp.blogspot.com/-X6peGqFZFZ0/UcTqFQ2O21I/AAAAAAAAPi0/uRJyfIgg9uo/s1600/blindssuck.gif - (Task.succeed <| - MsgPusher <| - Pusher.Model.Login - (config.pusherKey) - (Pusher.Model.AccessToken accessToken) - ) - |> Task.perform identity + case webDataUser of + Success _ -> + case model.config of + Success config -> + -- TODO: Surely it's possible to clean up the + -- Login call. This part was written using this + -- method: http://2.bp.blogspot.com/-X6peGqFZFZ0/UcTqFQ2O21I/AAAAAAAAPi0/uRJyfIgg9uo/s1600/blindssuck.gif + (Task.succeed <| + MsgPusher <| + Pusher.Model.Login + (config.pusherKey) + (Pusher.Model.AccessToken accessToken) + ) + |> Task.perform identity + + _ -> + Cmd.none _ -> Cmd.none @@ -284,11 +283,6 @@ subscriptions model = port accessTokenPort : String -> Cmd msg -{-| Send Pusher key and cluster to JS. --} -port pusherKey : ( String, String, List String ) -> Cmd msg - - {-| Get a singal if internet connection is lost. -} port offline : (Value -> msg) -> Sub msg diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index 4f35134..b0271e4 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -49,6 +49,7 @@ type alias PusherConfig = , cluster : String , authEndpoint : String , channel : String + , eventNames : List String } diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm index 37dadb2..bca6a8a 100644 --- a/client/src/elm/Pusher/Update.elm +++ b/client/src/elm/Pusher/Update.elm @@ -2,6 +2,7 @@ port module Pusher.Update exposing (update, subscription) import Config.Model exposing (BackendUrl) import Pusher.Model exposing (..) +import Pusher.Utils exposing (getClusterName) {-| Login to pusher. @@ -109,9 +110,10 @@ update backendUrl pusherChannel msg model = let pusherConfig = { key = pusherAppKey.key - , cluster = "eu" + , cluster = getClusterName pusherAppKey.cluster , authEndpoint = backendUrl ++ "/api/pusher_auth?access_token=" ++ accessToken , channel = pusherChannel + , eventNames = Pusher.Model.eventNames } in ( model diff --git a/client/src/js/app.js b/client/src/js/app.js index 92fc176..1552cb8 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -58,12 +58,8 @@ elmApp.ports.pusherLogin.subscribe(function(config) { if (!pusher.channel(config.channel)) { var channel = pusher.subscribe(config.channel); - var eventNames = ['item__update']; - - eventNames.forEach(function(eventName) { + config.eventNames.forEach(function(eventName) { channel.bind(eventName, function(data) { - // We wrap the data with some information which will - // help us dispatch it on the Elm side var event = { eventType: eventName, data: data From d0b4b32bceaa18470a70e6ddd0522385a6305374 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sat, 8 Jul 2017 22:56:53 +0200 Subject: [PATCH 08/23] Updating dummyUser --- client/src/elm/App/Test.elm | 6 +++++- client/src/elm/Pusher/Model.elm | 8 -------- client/src/elm/Pusher/Update.elm | 1 - 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/client/src/elm/App/Test.elm b/client/src/elm/App/Test.elm index 67eef61..2b8d720 100644 --- a/client/src/elm/App/Test.elm +++ b/client/src/elm/App/Test.elm @@ -44,7 +44,11 @@ getPageAsAuthenticated : Page -> Page getPageAsAuthenticated page = let dummyUser = - { id = 100, name = "Foo", avatarUrl = "https://example.com" } + { id = 100 + , name = "Foo" + , avatarUrl = "https://example.com" + , pusherChannel = "general" + } model = { emptyModel | user = Success dummyUser } diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index b0271e4..d6ccccb 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -53,14 +53,6 @@ type alias PusherConfig = } -{-| Represents the state of our pusher connection. -This mostly tracks -The `(Maybe Int)` parameters track when the next reconnection attempt will take -place, if that is known. -We'll start in `Initialized` state, and stay there until we get a `Login` -message. At that point, we'll gradually proceed through `Connecting` to -`Connected` (if all goes well). --} type ConnectionStatus = Initialized | Connecting (Maybe Int) diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm index bca6a8a..7625cdf 100644 --- a/client/src/elm/Pusher/Update.elm +++ b/client/src/elm/Pusher/Update.elm @@ -62,7 +62,6 @@ update backendUrl pusherChannel msg model = ) HandleError error -> - -- We could consider keeping only X number of errors ( { model | errors = error :: model.errors } , Cmd.none ) From 54475bf172c480e39b1b950124b18a394904bf59 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sat, 8 Jul 2017 23:10:19 +0200 Subject: [PATCH 09/23] Sniffer fixes --- .../plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc index 312827d..b02fd95 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc @@ -1,8 +1,10 @@ t('Pusher auth'), 'description' => t('Authenticate user for the pusher privileged channel.'), From 0a742522680cd22c52d5c03d46794fc0cdbf0aff Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sun, 9 Jul 2017 13:59:54 +0200 Subject: [PATCH 10/23] Cleaning up pusher login call [skip ci] --- client/src/elm/App/Model.elm | 1 + client/src/elm/App/Update.elm | 66 ++++++++++++++++---------------- client/src/elm/Pusher/Model.elm | 6 ++- client/src/elm/Pusher/Update.elm | 6 +-- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/client/src/elm/App/Model.elm b/client/src/elm/App/Model.elm index f2c6bf1..e429dde 100644 --- a/client/src/elm/App/Model.elm +++ b/client/src/elm/App/Model.elm @@ -28,6 +28,7 @@ type Msg | SetCurrentDate Date | Tick Time | ToggleSideBar + | NoOp type alias Model = diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index 24e5dce..d635494 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -120,16 +120,8 @@ update msg model = MsgPusher subMsg -> let - pusherChannel = - case model.user of - Success user -> - user.pusherChannel - - _ -> - "general" - ( val, cmd ) = - Pusher.Update.update backendUrl pusherChannel subMsg model.pusher + Pusher.Update.update backendUrl subMsg model.pusher in ( { model | pusher = val } , Cmd.map MsgPusher cmd @@ -171,35 +163,13 @@ update msg model = _ -> modelUpdated ! [] - - pusherMsg = - case webDataUser of - Success _ -> - case model.config of - Success config -> - -- TODO: Surely it's possible to clean up the - -- Login call. This part was written using this - -- method: http://2.bp.blogspot.com/-X6peGqFZFZ0/UcTqFQ2O21I/AAAAAAAAPi0/uRJyfIgg9uo/s1600/blindssuck.gif - (Task.succeed <| - MsgPusher <| - Pusher.Model.Login - (config.pusherKey) - (Pusher.Model.AccessToken accessToken) - ) - |> Task.perform identity - - _ -> - Cmd.none - - _ -> - Cmd.none in ( modelWithRedirect , Cmd.batch [ Cmd.map PageLogin cmds , accessTokenPort accessToken , setActivePageCmds - , pusherMsg + , pusherLogin model webDataUser accessToken ] ) @@ -239,6 +209,9 @@ update msg model = ToggleSideBar -> { model | sidebarOpen = not model.sidebarOpen } ! [] + NoOp -> + model ! [] + {-| Determine is a page can be accessed by a user (anonymous or authenticated), and if not return a access denied page. @@ -286,3 +259,32 @@ port accessTokenPort : String -> Cmd msg {-| Get a singal if internet connection is lost. -} port offline : (Value -> msg) -> Sub msg + + +pusherLogin : Model -> WebData User -> String -> Cmd Msg +pusherLogin model webDataUser accessToken = + let + pusherLoginMsg pusherKey pusherChannel = + MsgPusher <| + Pusher.Model.Login + pusherKey + pusherChannel + (Pusher.Model.AccessToken accessToken) + + msg = + case webDataUser of + Success user -> + case model.config of + Success config -> + pusherLoginMsg config.pusherKey user.pusherChannel + + _ -> + NoOp + + _ -> + NoOp + + ( _, pusherLogin ) = + update msg model + in + pusherLogin diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index d6ccccb..4269085 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -44,6 +44,10 @@ type AccessToken = AccessToken String +type alias PusherChannel = + String + + type alias PusherConfig = { key : String , cluster : String @@ -82,5 +86,5 @@ type Msg | HandleConnectingIn Int | ShowErrorModal | HideErrorModal - | Login PusherAppKey AccessToken + | Login PusherAppKey PusherChannel AccessToken | Logout diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm index 7625cdf..6674bca 100644 --- a/client/src/elm/Pusher/Update.elm +++ b/client/src/elm/Pusher/Update.elm @@ -41,8 +41,8 @@ subscription = ] -update : BackendUrl -> String -> Msg -> Model -> ( Model, Cmd Msg ) -update backendUrl pusherChannel msg model = +update : BackendUrl -> Msg -> Model -> ( Model, Cmd Msg ) +update backendUrl msg model = case msg of HandleConnectingIn delay -> let @@ -105,7 +105,7 @@ update backendUrl pusherChannel msg model = , Cmd.none ) - Login pusherAppKey (AccessToken accessToken) -> + Login pusherAppKey pusherChannel (AccessToken accessToken) -> let pusherConfig = { key = pusherAppKey.key From a539ed145d5f2d1e85e2b88cb68d13b6485bd94c Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sun, 9 Jul 2017 14:40:25 +0200 Subject: [PATCH 11/23] Adding private note on items [skip ci] --- .../hedley_item.features.field_base.inc | 19 +++++++++ .../hedley_item.features.field_instance.inc | 42 +++++++++++++++++++ .../custom/hedley_item/hedley_item.info | 5 ++- .../custom/hedley_item/hedley_item.module | 35 +++++++++++++--- .../hedley_item/hedley_item.strongarm.inc | 8 ++-- .../custom/hedley_migrate/csv/item.csv | 8 ++-- .../handlers/node/HedleyMigrateItems.php | 2 + .../custom/hedley_pusher/hedley_pusher.module | 11 +++++ .../hedley_pusher/tests/HedleyPusherAuth.test | 2 +- .../restful/node/HedleyRestfulItems.class.php | 2 +- 10 files changed, 116 insertions(+), 18 deletions(-) diff --git a/server/hedley/modules/custom/hedley_item/hedley_item.features.field_base.inc b/server/hedley/modules/custom/hedley_item/hedley_item.features.field_base.inc index bcff7c0..bee57a8 100644 --- a/server/hedley/modules/custom/hedley_item/hedley_item.features.field_base.inc +++ b/server/hedley/modules/custom/hedley_item/hedley_item.features.field_base.inc @@ -32,5 +32,24 @@ function hedley_item_field_default_field_bases() { 'type' => 'image', ); + // Exported field_base: 'field_private_note'. + $field_bases['field_private_note'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_private_note', + 'indexes' => array( + 'format' => array( + 0 => 'format', + ), + ), + 'locked' => 0, + 'module' => 'text', + 'settings' => array(), + 'translatable' => 0, + 'type' => 'text_long', + ); + return $field_bases; } diff --git a/server/hedley/modules/custom/hedley_item/hedley_item.features.field_instance.inc b/server/hedley/modules/custom/hedley_item/hedley_item.features.field_instance.inc index 4340b7b..398bfeb 100644 --- a/server/hedley/modules/custom/hedley_item/hedley_item.features.field_instance.inc +++ b/server/hedley/modules/custom/hedley_item/hedley_item.features.field_instance.inc @@ -60,9 +60,51 @@ function hedley_item_field_default_field_instances() { ), ); + // Exported field_instance: 'node-item-field_private_note'. + $field_instances['node-item-field_private_note'] = array( + 'bundle' => 'item', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => 'Accessible only for administrators.', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'text', + 'settings' => array(), + 'type' => 'text_default', + 'weight' => 7, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_private_note', + 'label' => 'Private note', + 'required' => 0, + 'settings' => array( + 'text_processing' => 0, + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'text', + 'settings' => array( + 'rows' => 5, + ), + 'type' => 'text_textarea', + 'weight' => 11, + ), + ); + // Translatables // Included for use with string extractors like potx. + t('Accessible only for administrators.'); t('Image'); + t('Private note'); return $field_instances; } diff --git a/server/hedley/modules/custom/hedley_item/hedley_item.info b/server/hedley/modules/custom/hedley_item/hedley_item.info index 9c5d9b3..a1f5088 100644 --- a/server/hedley/modules/custom/hedley_item/hedley_item.info +++ b/server/hedley/modules/custom/hedley_item/hedley_item.info @@ -11,16 +11,17 @@ dependencies[] = number dependencies[] = strongarm features[ctools][] = strongarm:strongarm:1 features[features_api][] = api:2 -features[field_base][] = field_contacts features[field_base][] = field_image +features[field_base][] = field_private_note features[field_instance][] = node-item-field_image +features[field_instance][] = node-item-field_private_note features[node][] = item features[variable][] = comment_anonymous_item features[variable][] = comment_default_mode_item features[variable][] = comment_default_per_page_item features[variable][] = comment_form_location_item -features[variable][] = comment_preview_item features[variable][] = comment_item +features[variable][] = comment_preview_item features[variable][] = comment_subject_field_item features[variable][] = field_bundle_settings_node__item features[variable][] = menu_options_item diff --git a/server/hedley/modules/custom/hedley_item/hedley_item.module b/server/hedley/modules/custom/hedley_item/hedley_item.module index fbd4c6b..44c2bd8 100644 --- a/server/hedley/modules/custom/hedley_item/hedley_item.module +++ b/server/hedley/modules/custom/hedley_item/hedley_item.module @@ -43,15 +43,38 @@ function hedley_item_trigger_pusher_on_item_data_change($node) { return; } - $account = user_load($node->uid); - + // Load the item view from restful, as the item author. $handler = restful_get_restful_handler('items'); - $handler->setAccount($account); + $handler->setAccount(user_load($node->uid)); $data = $handler->get($node->nid); - + // Remove the private note, in case the author is an admin. + if (array_key_exists('private_note', $data[0])) { + unset($data[0]['private_note']); + } hedley_pusher_trigger_event('general', 'item__update', $data[0]); - // Send dummy private event. - $data[0]['private_field'] = 'Here be secrets'; + // Send an event to the private channel, with the private note field. + $wrapper = entity_metadata_wrapper('node', $node); + $data[0]['private_note'] = $wrapper->field_private_note->value(); hedley_pusher_trigger_event('private-general', 'item__update', $data[0]); } + +/** + * Implements hook_permission(). + */ +function hedley_item_permission() { + return [ + 'access private fields' => [ + 'title' => t('Access private fields.'), + ], + ]; +} + +/** + * Implements hook_field_access(). + */ +function hedley_item_field_access($op, $field, $entity_type, $entity, $account) { + if ($field['field_name'] == 'field_private_note') { + return user_access('access private fields', $account); + } +} diff --git a/server/hedley/modules/custom/hedley_item/hedley_item.strongarm.inc b/server/hedley/modules/custom/hedley_item/hedley_item.strongarm.inc index ebab8e0..06aa9cf 100644 --- a/server/hedley/modules/custom/hedley_item/hedley_item.strongarm.inc +++ b/server/hedley/modules/custom/hedley_item/hedley_item.strongarm.inc @@ -41,16 +41,16 @@ function hedley_item_strongarm() { $strongarm = new stdClass(); $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */ $strongarm->api_version = 1; - $strongarm->name = 'comment_preview_item'; + $strongarm->name = 'comment_item'; $strongarm->value = '1'; - $export['comment_preview_item'] = $strongarm; + $export['comment_item'] = $strongarm; $strongarm = new stdClass(); $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */ $strongarm->api_version = 1; - $strongarm->name = 'comment_item'; + $strongarm->name = 'comment_preview_item'; $strongarm->value = '1'; - $export['comment_item'] = $strongarm; + $export['comment_preview_item'] = $strongarm; $strongarm = new stdClass(); $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */ diff --git a/server/hedley/modules/custom/hedley_migrate/csv/item.csv b/server/hedley/modules/custom/hedley_migrate/csv/item.csv index 49e3499..7dc26b4 100644 --- a/server/hedley/modules/custom/hedley_migrate/csv/item.csv +++ b/server/hedley/modules/custom/hedley_migrate/csv/item.csv @@ -1,4 +1,4 @@ -title,field_image -Kitchen,kitchen.jpg -West Garden,west-garden.jpg -XSS Test,kitchen.jpg +title,field_image,field_private_note +Kitchen,kitchen.jpg,Secret note +West Garden,west-garden.jpg, +XSS Test,kitchen.jpg, diff --git a/server/hedley/modules/custom/hedley_migrate/handlers/node/HedleyMigrateItems.php b/server/hedley/modules/custom/hedley_migrate/handlers/node/HedleyMigrateItems.php index 8bd982b..64e7bc3 100644 --- a/server/hedley/modules/custom/hedley_migrate/handlers/node/HedleyMigrateItems.php +++ b/server/hedley/modules/custom/hedley_migrate/handlers/node/HedleyMigrateItems.php @@ -26,6 +26,7 @@ public function __construct($arguments) { $column_names = [ 'title', 'field_image', + 'field_private_note', ]; $columns = []; @@ -51,6 +52,7 @@ public function __construct($arguments) { $simple_fields = drupal_map_assoc([ 'title', + 'field_private_note', ]); $this->addSimpleMappings($simple_fields); diff --git a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module index 62b9f96..4002d0a 100644 --- a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module +++ b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module @@ -5,6 +5,17 @@ * Code for the Hedley pusher feature. */ +/** + * Implements hook_permission(). + */ +function hedley_pusher_permission() { + return [ + 'access private pusher channel' => [ + 'title' => t('Access the private pusher channel.'), + ], + ]; +} + /** * Trigger a Pusher event. * diff --git a/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test b/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test index 5974c11..f2f8174 100644 --- a/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test +++ b/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test @@ -84,7 +84,7 @@ class HedleyPusherAuth extends DrupalWebTestCase { $this->fail('Posting to the pusher auth endpoint with an unprivileged user should have thrown an exception.'); } catch (\RestfulForbiddenException $e) { - $this->assertTrue(strstr($e->getMessage(), 'no "access private channel" permission'), "The exception was thrown because the user don't have the needed permission."); + $this->assertTrue(strstr($e->getMessage(), 'no "access private pusher channel" permission'), "The exception was thrown because the user don't have the needed permission."); } // Auth with an admin, and check that a token is retrieved. diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/node/HedleyRestfulItems.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/node/HedleyRestfulItems.class.php index 715f2c6..c694295 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/node/HedleyRestfulItems.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/node/HedleyRestfulItems.class.php @@ -16,7 +16,7 @@ class HedleyRestfulItems extends HedleyRestfulEntityBaseNode { public function publicFieldsInfo() { $public_fields = parent::publicFieldsInfo(); - $field_names = []; + $field_names = ['field_private_note']; foreach ($field_names as $field_name) { $public_name = str_replace('field_', '', $field_name); From 95384b8ea6f7c4d7db2e3be8a621997843f9d8f3 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sun, 9 Jul 2017 15:00:01 +0200 Subject: [PATCH 12/23] Displaying private note --- client/src/elm/Item/Decoder.elm | 1 + client/src/elm/Item/Model.elm | 1 + client/src/elm/Pages/Item/View.elm | 46 ++++++++++++++++++------------ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/client/src/elm/Item/Decoder.elm b/client/src/elm/Item/Decoder.elm index 594f79e..1442b1b 100644 --- a/client/src/elm/Item/Decoder.elm +++ b/client/src/elm/Item/Decoder.elm @@ -15,6 +15,7 @@ decodeItem = decode Item |> required "label" string |> optionalAt [ "image", "styles", "large" ] string "http://placehold.it/350x150" + |> optional "private_note" (nullable string) Nothing decodeItemsDict : Decoder ItemsDict diff --git a/client/src/elm/Item/Model.elm b/client/src/elm/Item/Model.elm index 2f9e782..d80a2f0 100644 --- a/client/src/elm/Item/Model.elm +++ b/client/src/elm/Item/Model.elm @@ -15,6 +15,7 @@ type alias ItemId = type alias Item = { name : String , image : String + , privateNote : Maybe String } diff --git a/client/src/elm/Pages/Item/View.elm b/client/src/elm/Pages/Item/View.elm index 8881cd5..5d38422 100644 --- a/client/src/elm/Pages/Item/View.elm +++ b/client/src/elm/Pages/Item/View.elm @@ -10,23 +10,33 @@ import User.Model exposing (User) view : Date -> User -> ItemId -> Item -> Html Msg view currentDate currentUser itemId item = - div [] - [ div - [ class "ui secondary pointing fluid menu" ] - [ h2 - [ class "ui header" ] - [ text item.name ] - , div - [ class "right menu" ] - [ a - [ class "ui active item" ] - [ text "Overview" ] + let + privateNote = + case item.privateNote of + Just note -> + div [] [ text note ] + + _ -> + text "" + in + div [] + [ div + [ class "ui secondary pointing fluid menu" ] + [ h2 + [ class "ui header" ] + [ text item.name ] + , div + [ class "right menu" ] + [ a + [ class "ui active item" ] + [ text "Overview" ] + ] ] + , div [] + [ img [ src item.image, alt item.name ] [] + ] + , div + [ class "ui divider" ] + [] + , privateNote ] - , div [] - [ img [ src item.image, alt item.name ] [] - ] - , div - [ class "ui divider" ] - [] - ] From 006bb539eba194614dcd564b262b641243f29fdf Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sun, 9 Jul 2017 15:36:41 +0200 Subject: [PATCH 13/23] Removing unused pusher parts, and wiring pusher logout --- client/src/elm/App/Update.elm | 21 +++++++++++++------- client/src/elm/Pusher/Model.elm | 5 ----- client/src/elm/Pusher/Update.elm | 33 -------------------------------- client/src/js/app.js | 4 ---- 4 files changed, 14 insertions(+), 49 deletions(-) diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index d635494..21e1e76 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -81,13 +81,20 @@ update msg model = model ! [] Logout -> - ( { emptyModel - | accessToken = "" - , activePage = Login - , config = model.config - } - , accessTokenPort "" - ) + let + ( _, pusherLogout ) = + update (MsgPusher Pusher.Model.Logout) model + in + ( { emptyModel + | accessToken = "" + , activePage = Login + , config = model.config + } + , Cmd.batch + [ accessTokenPort "" + , pusherLogout + ] + ) MsgItemManager subMsg -> case model.user of diff --git a/client/src/elm/Pusher/Model.elm b/client/src/elm/Pusher/Model.elm index 4269085..a7e69c7 100644 --- a/client/src/elm/Pusher/Model.elm +++ b/client/src/elm/Pusher/Model.elm @@ -6,7 +6,6 @@ import Item.Model exposing (Item, ItemId) type alias Model = { connectionStatus : ConnectionStatus , errors : List PusherError - , showErrorModal : Bool } @@ -14,7 +13,6 @@ emptyModel : Model emptyModel = { connectionStatus = Initialized , errors = [] - , showErrorModal = False } @@ -83,8 +81,5 @@ eventNames = type Msg = HandleError PusherError | HandleStateChange String - | HandleConnectingIn Int - | ShowErrorModal - | HideErrorModal | Login PusherAppKey PusherChannel AccessToken | Logout diff --git a/client/src/elm/Pusher/Update.elm b/client/src/elm/Pusher/Update.elm index 6674bca..a31b417 100644 --- a/client/src/elm/Pusher/Update.elm +++ b/client/src/elm/Pusher/Update.elm @@ -25,11 +25,6 @@ port pusherError : (PusherError -> msg) -> Sub msg port pusherState : (String -> msg) -> Sub msg -{-| Receive notice of upcoming connection attempts. --} -port pusherConnectingIn : (Int -> msg) -> Sub msg - - {-| Subscription to connection status. -} subscription : Sub Msg @@ -37,30 +32,12 @@ subscription = Sub.batch [ pusherError HandleError , pusherState HandleStateChange - , pusherConnectingIn HandleConnectingIn ] update : BackendUrl -> Msg -> Model -> ( Model, Cmd Msg ) update backendUrl msg model = case msg of - HandleConnectingIn delay -> - let - connectionStatus = - case model.connectionStatus of - Connecting _ -> - Connecting (Just delay) - - Unavailable _ -> - Unavailable (Just delay) - - _ -> - model.connectionStatus - in - ( { model | connectionStatus = connectionStatus } - , Cmd.none - ) - HandleError error -> ( { model | errors = error :: model.errors } , Cmd.none @@ -95,16 +72,6 @@ update backendUrl msg model = , Cmd.none ) - ShowErrorModal -> - ( { model | showErrorModal = True } - , Cmd.none - ) - - HideErrorModal -> - ( { model | showErrorModal = False } - , Cmd.none - ) - Login pusherAppKey pusherChannel (AccessToken accessToken) -> let pusherConfig = diff --git a/client/src/js/app.js b/client/src/js/app.js index 1552cb8..00a0812 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -51,10 +51,6 @@ elmApp.ports.pusherLogin.subscribe(function(config) { elmApp.ports.pusherState.send(states.current); }); - pusher.connection.bind('connecting_in', function (delay) { - elmApp.ports.pusherConnectingIn.send(delay); - }); - if (!pusher.channel(config.channel)) { var channel = pusher.subscribe(config.channel); From fde3b02897610623d5aeab7bd33729f5b076e5fb Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Sun, 9 Jul 2017 15:59:23 +0200 Subject: [PATCH 14/23] Fixing pusher test --- client/src/elm/Pusher/Test.elm | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/elm/Pusher/Test.elm b/client/src/elm/Pusher/Test.elm index fc0ab7a..fa9b295 100644 --- a/client/src/elm/Pusher/Test.elm +++ b/client/src/elm/Pusher/Test.elm @@ -32,6 +32,7 @@ decodeTest = , data = { name = "new-item" , image = "http://placehold.it/350x150" + , privateNote = Nothing } |> ItemUpdate } From 7e77f7c6540b6dd130549c66a0e9fff065d354c1 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 10:39:23 +0200 Subject: [PATCH 15/23] Updating the pusher model when calling pusherLogin and pusherLogout [skip ci] --- client/src/elm/App/Update.elm | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index 21e1e76..19d7c5f 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -82,17 +82,18 @@ update msg model = Logout -> let - ( _, pusherLogout ) = + ( modelUpdated, pusherLogoutCmd ) = update (MsgPusher Pusher.Model.Logout) model in ( { emptyModel | accessToken = "" , activePage = Login , config = model.config + , pusher = modelUpdated.pusher } , Cmd.batch [ accessTokenPort "" - , pusherLogout + , pusherLogoutCmd ] ) @@ -139,10 +140,14 @@ update msg model = ( val, cmds, ( webDataUser, accessToken ) ) = Pages.Login.Update.update backendUrl msg model.pageLogin + ( pusherModelUpdated, pusherLoginCmd ) = + pusherLogin model webDataUser accessToken + modelUpdated = { model | pageLogin = val , accessToken = accessToken + , pusher = pusherModelUpdated , user = webDataUser } @@ -176,7 +181,7 @@ update msg model = [ Cmd.map PageLogin cmds , accessTokenPort accessToken , setActivePageCmds - , pusherLogin model webDataUser accessToken + , pusherLoginCmd ] ) @@ -268,9 +273,14 @@ port accessTokenPort : String -> Cmd msg port offline : (Value -> msg) -> Sub msg -pusherLogin : Model -> WebData User -> String -> Cmd Msg +{-| Login to pusher. +Either subscribes to the private channel, or to the general channel, according +to user.pusherChannel. +-} +pusherLogin : Model -> WebData User -> String -> ( Pusher.Model.Model, Cmd Msg ) pusherLogin model webDataUser accessToken = let + -- Create the pusher login Msg, wrapped as a MsgPusher. pusherLoginMsg pusherKey pusherChannel = MsgPusher <| Pusher.Model.Login @@ -278,6 +288,8 @@ pusherLogin model webDataUser accessToken = pusherChannel (Pusher.Model.AccessToken accessToken) + -- Create a MsgPusher for login, or NoOp in case the user or the config + -- are missing. msg = case webDataUser of Success user -> @@ -291,7 +303,9 @@ pusherLogin model webDataUser accessToken = _ -> NoOp - ( _, pusherLogin ) = + ( updatedModel, pusherLoginCmd ) = update msg model in - pusherLogin + -- Return the pusher part of the model, as the pusher login action + -- shouldn't change other parts. + ( updatedModel.pusher, pusherLoginCmd ) From 3babb2e49cadbab976c0f1896a036526a8fe78c9 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 11:09:19 +0200 Subject: [PATCH 16/23] Ordering msgs [skip ci] --- client/src/elm/App/Model.elm | 2 +- client/src/elm/App/Update.elm | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/elm/App/Model.elm b/client/src/elm/App/Model.elm index e429dde..5c6b827 100644 --- a/client/src/elm/App/Model.elm +++ b/client/src/elm/App/Model.elm @@ -23,12 +23,12 @@ type Msg | Logout | MsgItemManager ItemManager.Model.Msg | MsgPusher Pusher.Model.Msg + | NoOp | PageLogin Pages.Login.Model.Msg | SetActivePage Page | SetCurrentDate Date | Tick Time | ToggleSideBar - | NoOp type alias Model = diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index 19d7c5f..be1f32a 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -135,6 +135,9 @@ update msg model = , Cmd.map MsgPusher cmd ) + NoOp -> + model ! [] + PageLogin msg -> let ( val, cmds, ( webDataUser, accessToken ) ) = @@ -221,9 +224,6 @@ update msg model = ToggleSideBar -> { model | sidebarOpen = not model.sidebarOpen } ! [] - NoOp -> - model ! [] - {-| Determine is a page can be accessed by a user (anonymous or authenticated), and if not return a access denied page. From 034394c856cdf83346273d67d31fe1e9bcb658db Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 11:23:23 +0200 Subject: [PATCH 17/23] Cleaning up item view [skip ci] --- client/src/elm/Pages/Item/View.elm | 46 ++++++++++++------------------ client/src/elm/Utils/Html.elm | 15 +++++++++- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/client/src/elm/Pages/Item/View.elm b/client/src/elm/Pages/Item/View.elm index 5d38422..76aaf49 100644 --- a/client/src/elm/Pages/Item/View.elm +++ b/client/src/elm/Pages/Item/View.elm @@ -6,37 +6,27 @@ import Html.Attributes exposing (..) import Pages.Item.Model exposing (Msg(..)) import Item.Model exposing (ItemId, Item) import User.Model exposing (User) +import Utils.Html exposing (divider) view : Date -> User -> ItemId -> Item -> Html Msg view currentDate currentUser itemId item = - let - privateNote = - case item.privateNote of - Just note -> - div [] [ text note ] - - _ -> - text "" - in - div [] - [ div - [ class "ui secondary pointing fluid menu" ] - [ h2 - [ class "ui header" ] - [ text item.name ] - , div - [ class "right menu" ] - [ a - [ class "ui active item" ] - [ text "Overview" ] - ] - ] - , div [] - [ img [ src item.image, alt item.name ] [] - ] + div [] + [ div + [ class "ui secondary pointing fluid menu" ] + [ h2 + [ class "ui header" ] + [ text item.name ] , div - [ class "ui divider" ] - [] - , privateNote + [ class "right menu" ] + [ a + [ class "ui active item" ] + [ text "Overview" ] + ] + ] + , div [] + [ img [ src item.image, alt item.name ] [] ] + , divider + , div [] [ text <| Maybe.withDefault "" item.privateNote ] + ] diff --git a/client/src/elm/Utils/Html.elm b/client/src/elm/Utils/Html.elm index 4a94b4a..f7db558 100644 --- a/client/src/elm/Utils/Html.elm +++ b/client/src/elm/Utils/Html.elm @@ -1,8 +1,21 @@ -module Utils.Html exposing (emptyNode) +module Utils.Html exposing (divider, emptyNode, showMaybe) import Html exposing (..) +import Html.Attributes exposing (class) + + +divider : Html msg +divider = + div [ class "ui divider" ] [] emptyNode : Html msg emptyNode = text "" + + +{-| Show Maybe Html if Just, or empty node if Nothing. +-} +showMaybe : Maybe (Html msg) -> Html msg +showMaybe = + Maybe.withDefault emptyNode From e6d7ef6d7ec2a6f7d2c8e52edf5f9c718d3249f1 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 11:31:31 +0200 Subject: [PATCH 18/23] cleanup login message creation [skip ci] --- client/src/elm/App/Update.elm | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/client/src/elm/App/Update.elm b/client/src/elm/App/Update.elm index be1f32a..c67e88f 100644 --- a/client/src/elm/App/Update.elm +++ b/client/src/elm/App/Update.elm @@ -291,17 +291,14 @@ pusherLogin model webDataUser accessToken = -- Create a MsgPusher for login, or NoOp in case the user or the config -- are missing. msg = - case webDataUser of - Success user -> - case model.config of - Success config -> - pusherLoginMsg config.pusherKey user.pusherChannel - - _ -> - NoOp - - _ -> - NoOp + RemoteData.toMaybe webDataUser + |> Maybe.map + (\user -> + RemoteData.toMaybe model.config + |> Maybe.map (\config -> pusherLoginMsg config.pusherKey user.pusherChannel) + |> Maybe.withDefault NoOp + ) + |> Maybe.withDefault NoOp ( updatedModel, pusherLoginCmd ) = update msg model From 14507ffc9a1285dba10b1e1cf76cc575be9d903d Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 11:35:27 +0200 Subject: [PATCH 19/23] showMaybe privateNote [skip ci] --- client/src/elm/Pages/Item/View.elm | 43 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/client/src/elm/Pages/Item/View.elm b/client/src/elm/Pages/Item/View.elm index 76aaf49..80b74a0 100644 --- a/client/src/elm/Pages/Item/View.elm +++ b/client/src/elm/Pages/Item/View.elm @@ -6,27 +6,34 @@ import Html.Attributes exposing (..) import Pages.Item.Model exposing (Msg(..)) import Item.Model exposing (ItemId, Item) import User.Model exposing (User) -import Utils.Html exposing (divider) +import Utils.Html exposing (divider, showMaybe) view : Date -> User -> ItemId -> Item -> Html Msg view currentDate currentUser itemId item = - div [] - [ div - [ class "ui secondary pointing fluid menu" ] - [ h2 - [ class "ui header" ] - [ text item.name ] - , div - [ class "right menu" ] - [ a - [ class "ui active item" ] - [ text "Overview" ] + let + privateNote = + showMaybe <| + Maybe.map + (\note -> div [ class "private-note" ] [ text note ]) + item.privateNote + in + div [] + [ div + [ class "ui secondary pointing fluid menu" ] + [ h2 + [ class "ui header" ] + [ text item.name ] + , div + [ class "right menu" ] + [ a + [ class "ui active item" ] + [ text "Overview" ] + ] ] + , div [] + [ img [ src item.image, alt item.name ] [] + ] + , divider + , privateNote ] - , div [] - [ img [ src item.image, alt item.name ] [] - ] - , divider - , div [] [ text <| Maybe.withDefault "" item.privateNote ] - ] From 47cc8ff970a8a804109c2d4bbb2bf47015a47406 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 11:47:51 +0200 Subject: [PATCH 20/23] Fixes --- .../custom/hedley_pusher/hedley_pusher.module | 19 +++++++++++-------- .../me/1.0/HedleyRestfulMeResource.class.php | 14 ++++++-------- .../HedleyRestfulPusherAuthResource.class.php | 6 +++--- .../user/pusher_auth/1.0/pusher_auth__1_0.inc | 4 ++-- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module index 4002d0a..5da729a 100644 --- a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module +++ b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module @@ -75,13 +75,16 @@ function hedley_pusher_get_pusher() { */ function hedley_pusher_exit() { $queue = &drupal_static('hedley_pusher_static_queue', []); - if (!empty($queue)) { - try { - $pusher = hedley_pusher_get_pusher(); - $pusher->triggerBatch($queue); - } - catch (Exception $e) { - watchdog('hedley_pusher', 'Could not transmit Pusher events: !queue', ['!queue' => serialize($queue)]); - } + if (!$queue) { + // No registered pusher events. + return; + } + + try { + $pusher = hedley_pusher_get_pusher(); + $pusher->triggerBatch($queue); + } + catch (Exception $e) { + watchdog('hedley_pusher', 'Could not transmit Pusher events: !queue', ['!queue' => serialize($queue)]); } } diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php index 55703a1..de8b371 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php @@ -27,12 +27,12 @@ public function publicFieldsInfo() { unset($public_fields['self']); - $public_fields['pusher_channel'] = array( + $public_fields['pusher_channel'] = [ 'property' => 'uid', 'process_callbacks' => [ [$this, 'getPusherChannel'], ], - ); + ]; return $public_fields; } @@ -48,16 +48,14 @@ public function viewEntity($entity_id) { } /** - * Get pusher channel for user. - * - * @param int $uid - * User ID. + * Get pusher channel for the current user. * * @return string * The pusher channel name: 'general' or 'private-general'. */ - protected function getPusherChannel($uid) { - return user_access('access private pusher channel', user_load($uid)) ? 'private-general' : 'general'; + protected function getPusherChannel() { + $account = $this->getAccount(); + return user_access('access private pusher channel', $account) ? 'private-general' : 'general'; } } diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php index d8b96a6..936a08e 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php @@ -27,9 +27,9 @@ public static function controllersInfo() { public function publicFieldsInfo() { $public_fields = parent::publicFieldsInfo(); - $public_fields['auth'] = array( - 'callback' => array($this, 'getPusherAuth'), - ); + $public_fields['auth'] = [ + 'callback' => [$this, 'getPusherAuth'], + ]; return $public_fields; } diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc index b02fd95..dd4a1a5 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc @@ -5,7 +5,7 @@ * Pusher auth plugin definition. */ -$plugin = array( +$plugin = [ 'label' => t('Pusher auth'), 'description' => t('Authenticate user for the pusher privileged channel.'), 'resource' => 'pusher_auth', @@ -14,4 +14,4 @@ $plugin = array( 'bundle' => 'user', 'authentication_types' => TRUE, 'formatter' => 'simple', -); +]; From 73b2f889eceae680d18967533f84eda1da067ea1 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 12:01:45 +0200 Subject: [PATCH 21/23] Replacing "access private pusher channel" with "access private fields" --- .../modules/custom/hedley_pusher/hedley_pusher.module | 11 ----------- .../custom/hedley_pusher/tests/HedleyPusherAuth.test | 2 +- .../user/me/1.0/HedleyRestfulMeResource.class.php | 2 +- .../1.0/HedleyRestfulPusherAuthResource.class.php | 4 ++-- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module index 5da729a..d08e5aa 100644 --- a/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module +++ b/server/hedley/modules/custom/hedley_pusher/hedley_pusher.module @@ -5,17 +5,6 @@ * Code for the Hedley pusher feature. */ -/** - * Implements hook_permission(). - */ -function hedley_pusher_permission() { - return [ - 'access private pusher channel' => [ - 'title' => t('Access the private pusher channel.'), - ], - ]; -} - /** * Trigger a Pusher event. * diff --git a/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test b/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test index f2f8174..0043fc1 100644 --- a/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test +++ b/server/hedley/modules/custom/hedley_pusher/tests/HedleyPusherAuth.test @@ -84,7 +84,7 @@ class HedleyPusherAuth extends DrupalWebTestCase { $this->fail('Posting to the pusher auth endpoint with an unprivileged user should have thrown an exception.'); } catch (\RestfulForbiddenException $e) { - $this->assertTrue(strstr($e->getMessage(), 'no "access private pusher channel" permission'), "The exception was thrown because the user don't have the needed permission."); + $this->assertTrue(strstr($e->getMessage(), 'no "access private fields" permission'), "The exception was thrown because the user don't have the needed permission."); } // Auth with an admin, and check that a token is retrieved. diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php index de8b371..e6e8512 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/me/1.0/HedleyRestfulMeResource.class.php @@ -55,7 +55,7 @@ public function viewEntity($entity_id) { */ protected function getPusherChannel() { $account = $this->getAccount(); - return user_access('access private pusher channel', $account) ? 'private-general' : 'general'; + return user_access('access private fields', $account) ? 'private-general' : 'general'; } } diff --git a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php index 936a08e..e42bba8 100644 --- a/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php +++ b/server/hedley/modules/custom/hedley_restful/plugins/restful/user/pusher_auth/1.0/HedleyRestfulPusherAuthResource.class.php @@ -55,8 +55,8 @@ public function authenticatePusher() { } $account = $this->getAccount(); - if (!user_access('access private pusher channel', $account)) { - throw new \RestfulForbiddenException(format_string('User @name is trying to log into the privileged pusher channel "@channel", but has no "access private pusher channel" permission.', ['@name' => $account->name, '@channel' => $request['channel_name']])); + if (!user_access('access private fields', $account)) { + throw new \RestfulForbiddenException(format_string('User @name is trying to log into the privileged pusher channel "@channel", but has no "access private fields" permission.', ['@name' => $account->name, '@channel' => $request['channel_name']])); } return $this->view($account->uid); From 7dcf9be3ee6ea7631723912e01ce30ca72562fd3 Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 12:49:47 +0200 Subject: [PATCH 22/23] Adding information about the private pusher channel to the readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index c9e59e7..7cd01f4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,15 @@ For the backend, check the [server README.md](https://github.com/Gizra/drupal-el For the frontend, check the [client README.md](https://github.com/Gizra/drupal-elm-starter/blob/master/client/README.md) +## Pusher +Editing an item produces pusher messages on two different channels: `general` +and `private-general`. The private channel is only accessible by users with +`access private fields` permission, and currently exposes the item's "Private +note" field, which normal users can't access. + +Login for example with `admin` / `admin` to see the item private field, and get +notifications through the private channel. + ## Credits [Gizra](https://gizra.com) From 6c4909929298a93e2dbc4c9f7c748de7ec0471de Mon Sep 17 00:00:00 2001 From: Itamar Shapira Bar-Lev Date: Mon, 10 Jul 2017 13:11:31 +0200 Subject: [PATCH 23/23] Updating pusher description --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7cd01f4..286825b 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,21 @@ For the backend, check the [server README.md](https://github.com/Gizra/drupal-el For the frontend, check the [client README.md](https://github.com/Gizra/drupal-elm-starter/blob/master/client/README.md) ## Pusher +When the item will be updated (E.g. via the backend on +[/node/1/edit](http://localhost/drupal-elm-starter/server/www/node/1/edit)), +it will fire Pusher messages that will update the Elm application in real time. + Editing an item produces pusher messages on two different channels: `general` and `private-general`. The private channel is only accessible by users with `access private fields` permission, and currently exposes the item's "Private note" field, which normal users can't access. -Login for example with `admin` / `admin` to see the item private field, and get -notifications through the private channel. - +Log in to the Elm app for example with `admin` / `admin` to see also the item +private note field (on [/#/item/1](http://localhost:3000/#/item/1) for +example), and get notifications through the private channel. +Log in with a normal user (For exmaple `alice` / `alice`), to get notifications +through the public pusher channel. + ## Credits [Gizra](https://gizra.com)