Given a moz-extension:// URL opened by bookmark, how can I switch to tab using extension code? - firefox-addon-webextensions

This is related to some other questions I am working on.
Say, for whatever reason, a user has bookmarked a page (call it pageURL of the pattern moz-extensions://MY-OWN-WEBEXT-ID/*) intended to be opened from a browser action context menu, and opened it in a tab, then opened many other tabs and possibly other windows. User knows the extension tab exists somewhere and doesn't want to keep opening new bookmarks, and so wants to use the add-on's browser action context menu to find the extension page's tab. Likewise, I don't want my add-on to open a duplicate tab.
The problem, since the add-on did not create the tab (a bookmark did), I have no tab ID to pass to browser.tabs.update( WebExtTab.id, { active: true } ) or window ID to pass to browser.windows.update( WebExtWindow.id, { focused: true } ). (WebExtWindow referring to a WebExtensions browser.windows.Window object, not a browser window object.
I can use browser.extension.getViews( ) to generate a list of browser window objects (aka tabs), and checking each window.location.href find that indeed the URL (and thus tab) does exist (somewhere), but I can't use that window object to focus on the tab nor to get a tab ID for browser.tabs.update().
In the case of multiple browser windows, I can't even get the right browser window to raise up given that window object, because the window objects returned by getViews have no id property with which to call browser.windows.update(). Similar to the tabs problem.
Finally, I can't use browser.tabs.query( { 'url': pageURL } ) to find the tab ID, because the url option must conform to match patterns, which FORBID using the moz-extension:// scheme.
What would be exceptionally useful was if the WebExtensions API allowed an extension to find the tabs and windows of all pages that belong to itself, regardless if those pages were opened by the add-on, manually entered, a bookmark or clicking a link.
For example, given a pageURL conforming to moz-extension://MY-OWN-WEBEXT-ID/*, one could do a browser.tabs.query and/or a browser.windows.query on a url matching the above pattern, and return a WebExt tab/window object, respectively. If such a tab/window was not opened by the WebExt API (i.e. bookmark), then generate a new object (i.e. a pseudo-create), to populate with existing data (i.e. location.href, status flags, etc) and generate new data as needed (i.e. the ID numbers), such that the returned object is usable within the context of the API.
This would fill a gap in API coverage where certain methods (i.e. getViews) return dead-end browser objects which have no hooks and no connection with the WebExt API and are thus mostly useless.

The simple answer: ++RTFM. browser.windows.getAll() will allow you to populate the windows objects with tab info. You need the permissions: [ "tabs" ] in manifest.json to get the tab.url property. But other than that, all the windows and tab objects will have an ID so that you can trivially focus window and switch active tab!
Note: This requires Firefox 52.0+ to make use of the async/await feature. Otherwise, you just have to use function generators and promises. Also, I've omitted any error checking, for demonstration purposes, but it might be a good idea to put them back in later.
async function tabCreate ( opts ) {
var pageURL = browser.runtime.getURL( opts.page + '.html' );
var extWins = await browser.windows.getAll( { populate: true, windowTypes: [ 'normal' ] } );
// Look for tab by comparing url, if url matches (i.e. tab exists), then focus window and make tab active.
for ( var extWin of extWins ) {
for ( var extTab of extWin.tabs ) {
if ( pageURL === extTab.url ) {
console.log( `My Extension->tabCreate(): Window ${extWin.id}, Tab ${extTab.id}:\n\t${extTab.url}` );
browser.windows.update( extWin.id, { focused: true } );
browser.tabs.update( extTab.id, { active: true } );
return;
}
}
}
// Otherwise, create tab.
browser.tabs.create( { url: pageURL } );
}
Opinion: I wish I didn't have to give away the tabs permission just for this feature. It would be nice if we always got our own moz-extension://MY-OWN-WEBEXT-ID/* urls, and null URLs for other tabs, without permissions given to access all tabs, but oh well.
Example Usage:
function myWebExt_Options ( ) {
tabCreate( {
'page': 'options',
'panel': 1
} );
}
browser.contextMenus.create( {
title: 'Options',
contexts: [ 'browser_action' ],
onclick: myWebExt_Options
} );
Note: I've implemented this to expect options in an opts object that has a page property, which I use as a shorthand to generate the full page URL. This is because of another question which requires passing a message to the page, which I store in opts.panel. But none of that is necessary. It could be changed to a flat string, or use the full 'getURL' generated elsewhere as a parameter. Change to suit your need and style.

Related

SalesForce query returns results in Query Editor, but returns null from APEX code in Lightning component

I'm completely new to SalesForce and have inherited a report that's not working. Please excuse any incorrect terminology, since I'm learning about all this as I go. The report has three prompts: states, years, and members. All dropdowns are supposed to populate with data returned from functions in an APEX class. State, which populates from a picklist, and years, which is populated with a loop, work fine. Members, which populates from a SQL query, returns nothing. If I run the report without any prompts selected (which should return an unfiltered list of results from a SQL query), it also returns nothing. Both of the SQL queries return data when I execute them directly in the query editor in the developer console, but they return nothing when called from the APEX functions.
Here's the initialization code from the Lightning controller:
doInit: function (component, event, helper) {
var action = component.get('c.getTrcAccounts');
action.setCallback(this, function (response) {
var state = response.getState();
if (state === 'SUCCESS' && component.isValid()) {
component.set('v.trcAccList', response.getReturnValue());
}
helper.getLocationState(component, event);
helper.getYear(component, event);
});
$A.enqueueAction(action);
},
Here are the two helper functions referenced in that code:
getLocationState: function (component, event) {
var action = component.get('c.getLocationState');
action.setCallback(this, function (response) {
var state = response.getState();
if (state === 'SUCCESS') {
component.set('v.LocationStateList', response.getReturnValue());
}
});
$A.enqueueAction(action);
},
getYear: function (component, event) {
var action = component.get('c.yearsOptions');
action.setCallback(this, function (response) {
var state = response.getState();
if (state === 'SUCCESS') {
component.set('v.LocationYearList', response.getReturnValue());
}
});
$A.enqueueAction(action);
}
Here is the code from the APEX class that returns the data for those three prompts:
Global class DataTableLocations {
#AuraEnabled
Global static List<TRC_Account__c> getTrcAccounts(){
set<string> trcAccountSet = new set<string>();
List<TRC_Account__c> traccList = new List<TRC_Account__c>();
for(TRC_Account__c trcacc : [SELECT Id, Name from TRC_Account__c WHERE TRC_Member__c = True order by Name limit 50000]){
if(!trcAccountSet.contains(trcacc.Name)){
trcAccountSet.add(trcacc.Name);
traccList.add(trcacc);
}
}
if(traccList.size()>0){
return traccList;
}
else{
return null;
}
}
#AuraEnabled
Global static List<string> getLocationState(){
List<string> options = new List<string>();
//options.add(new SelectOption('SelectAll', 'Select All'));
for( Schema.PicklistEntry f : Location__c.Physical_Address_State__c.getDescribe().getPicklistValues()) {
options.add(f.getValue());
}
return options;
}
#AuraEnabled
Global static List<string> yearsOptions() {
List<string> options = new List<string>();
date OldDate= date.today().addYears(-18);
integer oldyear=OldDate.year();
for( integer i=0; i<19 ;i++) {
options.add(string.valueOf(oldyear));
oldyear++;
}
return options;
}
}
If I run SELECT Id, Name from TRC_Account__c WHERE TRC_Member__c = True order by Name limit 50000 directly in the query editor window in the developer console, I get 7 results. However, if I output the response.getReturnValue() for getTrcAccounts in the doInit function, it's null.
Any help is greatly appreciated, as we're in a bit of a time crunch in conjunction with a site redesign. I'm told these reports were working at one point, but no one knows when they stopped working, and we inherited this code from a different company that did the original development. Thank you!
UPDATE:
In case it helps, this is the code in the lightning app that I think is used on the public page:
<aura:application extends="ltng:outApp" access="GLOBAL" implements="ltng:allowGuestAccess">
<aura:dependency resource="c:SearchBinReceiptsByYear"/>
</aura:application>
Edit
Right, it's a public page, it's called "Salesforce Sites". It's exposed to whole world without having to log in. These have special security in place because most of the time you don't want to expose data like that. At best you'd display contact us form, maybe some documents to download, product catalog... It's all very locked down, default is to ban everything and then admin decides what's allowed. It's bit unusual to have a Visualforce page + Aura component but ok, it happens.
You (and any other internal user) can see the results if you'd access this page from within salesforce. Something like https://mydomain.my.salesforce.com/apex/SearchBinReceiptsByYear and for you the page will work fine, "just" not outside of salesforce.
When exposed like that on the web - there's no logged in user. There's special "[Site Name] Guest User", you can see them if you search "Sites" in Setup. It has a special profile, also with [Site Name] in it. And nasty thing is - it doesn't show on the list of Users or Profiles.
Your code broke when Salesforce (auto)activated a critical update. Probably this one: https://releasenotes.docs.salesforce.com/en-us/spring20/release-notes/rn_networks_secure_perms_guests.htm There are some good resources on the net if you Google "Secure Object Permissions for Guest Users", for example https://katiekodes.com/salesforce-spring-20-guest-user/
Ask your system administrator colleague or read up a bit about sharing rules.
You'll have to go to Setup -> Sharing Rules. There's a checkbox that caused your stuff to break and you can't untick it.
Scroll down to your TRC Account object and hit "New". You'll need to create something like this, but with your criteria (TRC Member equals true)
Save, wait a bit (it might take a while to recalculate the sharing, you'll get an email) and try the page.
If it still doesn't work you'll have to check the Guest user's profile, it might need permissions to Read TRC Accounts and their Name field.
If it's Salesforce Sites - try this to find it: https://help.salesforce.com/articleView?id=000334554&type=1&mode=1
If it's a Customer Portal, Community, Digital Experience (they renamed the product few times) - try with https://help.salesforce.com/articleView?id=sf.rss_config_guest_user_profile.htm&type=5
Original answer
It looks like it's running OK because accounts (members?) are fetched first and in that fetch's callback (what to do when data comes back from server) you have helper.getLocationState, helper.getYear. And you wrote that these populate OK. It's not the best performance code but it should get the job done.
In no specific order...
Does the whole thing work OK for sysadmins? Or is it broken for everybody? If it works for sysadmins it might be something to do with sharing, your sysadmin should know (Setup -> Sharing settings is where you control who can see what. Maybe "mortals" are not allowed to see any data? Typically sysadmins bypass it. As a quick & dirty test you can modify the class definition to global without sharing class DataTableLocations but it's a really ugly hack.
What happens if you open DeveloperConsole (upper right corner) while running this component, do you see any errors in the logs? What happens if in the console you go Debug -> Open ExecuteAnonymous and run this piece of code:
System.debug(DataTableLocations.getTrcAccounts());
Does it return something? Throw error?
You can go to Setup -> Debug Mode, tick the checkbox next to your user and save. This slows the system down a bit but lets you debug the javascript better. You can then sprinkle some debugger; or console.log statements in the source code and view what happens in your browser's console (Ctrl+Shift+J in Chrome, Ctrl+Shift+I in firefox). For example
action.setCallback(this, function (response) {
var state = response.getState();
debugger;
console.log(state);
console.log(component.isValid());
console.table(response.getReturnValue());
if (state === 'SUCCESS' && component.isValid()) {
component.set('v.trcAccList', response.getReturnValue());
}
console.log(component.get('v.trcAccList'));
debugger;
helper.getLocationState(component, event);
helper.getYear(component, event);
});
How's the trcAccList variable actually used in the "cmp" file, in the HTML-like file? Maybe it's being set all right and contains 7 records but it's not displayed right?

Build optionally layouts in NextJS by detecting the browser URL value

all I would try to make some optional layout in NextJS. My problem is that I have found some method to check the browser URL then serve content according to these URL. But by this way the server content an browser content are no longer identical, hence the schema breaks.
Here an snippet of my trial:
export default ({children, title = 'title' }) => {
var
contact = false;
if (exenv.canUseDOM ){ if (window.location.href==="http://localhost:4000/contact" ) { contact= true}; }
else {const contact = false};
if (contact){ return( <div>Hey it is contact ! </div> ) }
else { // return normal layout
My console returns me predictably:
Warning: Text content did not match. Server:
So... I'm wondering if I have to make some custom settings on a custom NextJS server to grant the backend/frontend behaviors?
Or maybe there is a less expensive solution in the area?
Any hint would be great,
thanks
You can't remove that warning, it is appear because you just render that layout from client side, so there is a different between layout rendered by server and layout rendered by client.
I don't know what variable exenv.canUseDOM is, but you can use process.browser to detect if the current process is server side rendering or not.

Hide the default Gigya email share popup on click of the Gigya Share bar email icon

I am using the following code to implement the Set up Gigya share bar:
if ($window.gigya) {
// Step 1: Construct a UserAction object and fill it with data
var ua = new $window.gigya.socialize.UserAction();
ua.setLinkBack(articleUrl);
ua.setTitle($scope.title);
// Step 2: Define the Share Bar add-on's params object
var params = {
userAction: ua,
//shareButtons: 'linkedin,twitter,facebook,sina,email', // list of providers
shareButtons: 'linkedin,twitter,facebook,sina,email',
containerID: 'share-bar',
showCounts: 'none',
deviceType: 'auto',
cid: '',
wrap: true,
operationMode:'multiSelect',
onShareButtonClicked:function (e) {
console.log(e);
console.log(e.shareItem.provider);
if (e.shareItem.provider == 'email') {
var mailString = 'mailto:test#example.com'
$window.location.href = mailString;
}
}
};
// Step 3: Load the Share Bar add-on:
$window.gigya.socialize.showShareBarUI(params);
}
The above code displays the share bar provided by Gigya.
Now clicking the email option I am trying to open the default client mail (for example outlook). I see that the default email popup also get opened along with the outlook.
How to stop the default UI from opening in this case. I tried all the options but none are working for me.
Can anyone help me to know how to fix this issue.
I don't believe this behavior is supported. By design, the Share add-on, when sharing via email, constructs the actual message on the server using the UserAction passed in the request and then sends it from there. So, even if you got the default UI to not populate, how would you be getting the actual share data inside the 3rd party email program?

Why is session storage being drawn upon between different browser instances?

Background
In an application I'm working on, I've found that I can define values in sessionStorage in Chrome 62 on Windows 10, and that apparently changing that value in one tab affects other tabs that point to the same key.
I was operating under the assumption that localStorage is supposed to persist information across all browser windows, while sessionStorage is only supposed to persist information for a specific window or tab.
More specifically, I have an AngularJS service I'm using as a layer for sessionStorage interactions:
export class PersistenceSvc {
public static $inject: string[] = ['$window'];
public constructor(public $window: ng.IWindowService) {}
public save<T>(name: string, data: T): void {
const saveData: string = JSON.stringify(data);
this.$window.sessionStorage.setItem(name, saveData);
}
public load<T>(name: string): T {
const loadData: string = this.$window.sessionStorage.getItem(name);
const result: T = JSON.parse(loadData) as T;
return result;
}
}
...That I use from a run block in order to implement some data persistence in my application.
export function persistSomeData(
someSvc: Services.SomeService,
userAgentSvc: Services.UserAgentSvc,
persistenceSvc: Services.PersistenceSvc,
$window: ng.IWindowService) {
if(userAgentSvc.isMobileSafari()) {
// Special instructions for iOS devices.
return;
}
const dataToPersist: Models.DataModel = persistenceSvc.load<Models.DataModel>('SomeData');
if(dataToPersist) {
// Set up the state of someSvc with the data loaded.
} else {
// Phone home to the server to get the data needed.
}
$window.onbeforeunload = () => {
persistenceSvc.save<Models.DataModel>('SomeData', someSvc.dataState);
};
}
persistSomeData.$inject = [
// All requisite module names, omitted from example because lazy.
];
angular
.module('app')
.run(persistSomeData);
When only operating using a single tab, this works fine (unless running from an iOS device, but that's tangential to what I'm encountering.) When you do the following though, you start seeing some more interesting behavior...
Steps:
1. Open a Chrome instance. Create a new tab, and drag that out such that it becomes its own window.
2. Navigate to your site, that's using the above code.
3. Do things on your site that cause someSvc's data state to have different data in the first browser.
4. Do things on your site that cause someSvc's data state to have different data in the second browser.
5. Do something on your site that draws upon someSvc's data state in the first browser.
6. Observe that the data utilized on the first browser instance, was sourced by the second browser instance. (This is the problem, right here.)
Question:
In the past I haven't done a lot of cookie/localStorage/sessionStorage programming, so it's very possible that I've terribly misunderstood something. Bearing that in mind, why is it that window.sessionStorage is behaving in a way that the MDN documentation as well as the winning answer to this SO question says it shouldn't be behaving in?
EDIT: It turns out there is a problem, but it's not clientside. Closing this question, as I was operating under the assumption that the client was the problem.
There is something wrong with your code as a quick and easy test on the browser console shows that sessionStorage only impacts the browser tab that is open. A change in the right tab is not reflecting to the left tab:

Extjs - Generating a unique url for the tabs

I understand that ExtJS uses AJAX for all server side communication, and that ideally there would be only one page per application. But I am exploring the possibility of generating a unique url for a ExtJS tab which the user can then copy from the address bar for later use(traditional web application approach - making a page bookmarkable). Please let me know if anyone has done anything similar.
You can make use of the "hash". This is the portion of the URL which follows the "#" character.
If you only need to react to the hash at time of page load to support the bookmarking feature then you can get away with something like:
Ext.onReady(function() {
var tabPanel = new Ext.TabPanel({
// Configure for use in viewport as needed.
listeners: {
tabchange: function( tabPanel, tab ) {
window.location.hash = '#'+ tab.itemId;
}
}
});
var token = window.location.hash.substr(1);
if ( token ) {
var tab = tabPanel.get(token);
if ( ! tab ) {
// Create tab or error as necessary.
tab = new Ext.Panel({
itemId: token,
title: 'Tab: '+ token
});
tabPanel.add(tab);
}
tabPanel.setActiveTab(tab);
}
});
You may also choose to go further and employ the hashchange event supported in recent versions of most browsers. This will allow you to react to the hash being changed by either user or programmatic means after the page has finished loading:
if ( 'onhashchange' in window ) {
window.onhashchange = function() {
var token = window.location.hash.substr(1);
// Handle tab creation and activation as above.
}
}
It is worth noting that the Ext.History singleton promises functionality similar to this. However, as of ExtJS 3.3.1 it has not been given support for the hashchange event and is instead wholly dependent on a polling interval and a hidden iframe hack. I was not satisfied with its performance in modern browsers - IE in particular - until I rewrote it to use hashchange where available.

Resources