Author: Sarfraz Khan

Running PHP CLI commands for Magento on a hosted environment like Freehostia.com

If you want to run PHP CLI command for Magento extension installs on a hosted environment like Freehostia.com, and don’t have access to SSH or composer, cron jobs can be really helpful.

I had to install an extension and then run setup:upgrade, but didn’t have time to go through composer so instead, I created a cron job and put the following commands in:

/usr/local/php7.4/bin/php /home/www/bootwindows.com/bin/magento setup:upgrade

/usr/local/php7.4/bin/php /home/www/bootwindows.com/bin/magento setup:di:compile

/usr/local/php7.4/bin/php /home/www/bootwindows.com/bin/magento cache:clean


Once the cron jobs have run, you can simply delete the jobs and your magento install would have been upgraded.

Filed under: Blog

Azure Services in FedRAMP and DoD SRG Audit Scope – Azure Government | Microsoft Docs

Link to Azure Services in FedRAMP and DoD SRG Audit Scope. This section goes over offerings per the following audit scopes:

  • DoD CC SRG IL 2
  • DoD CC SRG IL 4
  • DoD CC SRG IL 5 (Azure Gov)*
  • DoD CC SRG IL 5 (Azure DoD) **
  • FedRAMP High
  • DoD CC SRG IL 6

CC SRG IL stands for Cloud Computing Security Requirements Group Impact Level. These levels are defined as follows:

Source: DISA

Filed under: BlogTagged with: , , ,

How to externally authenticate and consume data from Dynamics 365 CE Web API

There are 4 steps to authenticate and consume data from the D365 CE Web API in the cloud. In this scenario, we are going to use what is called the OAuth 2.0 Authorization Code Flow. It is one of the ways you can authenticate your calls. Authorization code flow is good because it’s hands-off. I will be using Postman to demonstrate these steps. The steps are:

  1. Authorize using password grant type.
  2. Get Token
  3. Use Refresh Token to get new Token (this will be used to get the token when it expires)
  4. Make a request to consume data

Step 1: Authorize using password grant type to get impersonation token.

Make a GET request to https://login.microsoftonline.com/common/oauth2/token with the header “content-type” = “application/x-www-form-urlencoded”. See Figure 1.

Figure 1: Headers

The body will include the client_id, resource, username, grant_type, client_secret, and password (See Figure 2). You can get the client_id and client_secret from the Azure Active Directory from an app that is authorized to access PowerPlatform, CDS, or Dynamics. The grant_type will be password.

You will get the access_token and refresh_token in response to this call. Note that the token will expire in 3599 seconds, which is 1 hour. This is where the refresh token will come in.

Figure 2

Step 2: Get the Token

The next step is to use the access token to get access token to consume the data. Make a POST request to:

https://login.microsoftonline.com/[[TENANTID]]/oauth2/token.

Note: You can get the tenant ID from Azure AD or from the login url when you’re authenticating into Microsoft products.

Get the access token from the first call and put it into the authorization field in Postman with TYPE being Bearer token. This will create a header that’s “authorization” : “Bearer [token]”. See Figure 3

Figure 3

Header remains content-type: application/x-www-form-urlencoded. See Figure 4.

Figure 4

The body this time will have the client_id, client_secret, grant_type of client_credentials, resource (url of your instance) and your tenant_id.

FIgure 5

The response will include an access token you can use to consume data from your resource. This is different from the access_token we received earlier because the scope of that token was only user_impersonation. This token will also expire in 1 hour. In the next step we will see how to get a new token from the refresh token.

Figure 6

Step 3: Get a new token using the refresh token

At this point, you can consume data from the Web API using the access token from step 2, but it will expire in 1 hour and if you want to keep getting a new password every hour, you’ll have to hard code your password in step 1. We want to avoid putting our passwords in places so we will use the refresh token to get a new password before it expires in the allotted 1 hour.

So we make a POST request to https://login.microsoftonline.com/[TENANTID]/oauth2/v2.0/token. The authorization type is bearer and we use the same access token from step 2. This will add a header “authorization”:”Bearer [token]” to the request. See figure 7.

Figure 7

The body includes client_id, resource, refresh_token, and secret (See figure 8). Note: we got the refresh token in step 1. Use this refresh token to get a new access token before it expires, and you won’t need to put step 1 in your code.

Figure 8

You will get a response with a new access token and refresh token with a 1-hour expiration time. No username or password required.

Figure 9

Step 4: Consume data

You can now continuously consume data from the web API using the access_token. Make a GET request to the resource you want. In my case, I will be pulling the top ten accounts. The authorization will be the Bearer Token (access_token from step 3). See figure 10.

Figure 10

You won’t need any specific headers besides the bearer token authorization which is being generated from the authorization above, but you can put in “content-type” of “application/json”, or other recommended OData headers if you want. Run the request and you’ll get your consumable data (See figure 11). You can also call actions or do CRUD operations with this token. Just make sure to refresh it before it expires.

Figure 11
Filed under: Blog

Open model-driven forms in “application mode”

I recently had a customer ask if they could replace InfoPath forms with Canvas Apps. That is a perfect use for canvas apps, but the users of those forms already had licenses to CE, so it got me thinking about using your regular model-driven forms as something that could pop up from an internal SharePoint site or an intranet.

Turns out it can simply be achieved through the removal of the command bar and navbar from the URL, and then the removal of the title bar and toolbar from the window.open function. A sample script is as follows.

window.open('https://[COMPANY].crm.dynamics.com/main.aspx?appid=00000000-0000-0000-0000-000000000000&cmdbar=false&forceUCI=1&navbar=off&pagetype=entityrecord&etn=contact','winname',"directories=0,titlebar=0,toolbar=0,location=0,status=0,menubar=0,scrollbars=no,resizable=no,width=1200,height=900");
Filed under: Blog

Close Opportunity in D365 CE with Alert.js 2.1

There is an article on Magnetism about how to use Alert.js 3.0 to mimic an opportunity close dialog. This is an example of how to use the open-source Alert.js 2.0 to mimic the opportunity close function. Alert.js 3.0 is $1200 and I definitely recommend it if you have the budget, but if not, you can still use 2.0 to do achieve similar functionality

So without further chit chat, here is what you do.

  1. Download and install Alert.js 2.1 on your CRM instance.
  2. Put the Opportunity entity in a solution without any sub-components.
  3. Create a JavaScript web resource and call it OpportunityClose.js or whatever you want to call it.
  4. Put the code listed below in the OpportunityClose.js
  5. Download and open Ribbon Workbench in XrmToolBox or your CRM instance.
  6. Add a button as seen in Image 1.
  7. Add a command as seen in Image 2.
  8. Create an Enable Rule and add it to the command as seen in Image 3.
Image 1: Adding the button. Note that the command will be empty until you create it and then add it in the button.
Image 2: Adding the command
Image 3: Adding a Display Rule to the command so that it only displays when the opportunity is active.
// Called from ribbon button, passing through first primary record ID
function showCloseAsWon(firstPrimaryItemId, primaryControl) {  
var context = primaryControl.context;
var pid = firstPrimaryItemId.replace( /[{}]/g, '' );

var title = "Close Opportunity"; 

debugger;
//Build Status Reason Option Set values
var options = "";
var req = new XMLHttpRequest();
req.open("GET", context.getClientUrl() + "/api/data/v9.1/stringmaps?$select=attributevalue,value&$filter=attributename eq 'statuscode' and  objecttypecode eq 'opportunity'", false);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("Prefer", "odata.include-annotations=\"*\"");
req.onreadystatechange = function() {
    if (this.readyState === 4) {
        req.onreadystatechange = null;
        if (this.status === 200) {
            var results = JSON.parse(this.response);
            for (var i = 0; i < results.value.length; i++) {
                var attributevalue = results.value[i]["attributevalue"];
                var value = results.value[i]["value"];
                var opt = "<option "+"id='" + value +"' value='"+ attributevalue.toString() +"'>"+ value +"</option>";
                options = options.concat(opt);
            }
        } else {
           options = "<option value='5'>Lost</option>";
        }
    }
};
req.send();
 
var style = "<style>" 
            + ".header { padding-top: 10px;}"
            + "p { font-size: 14px; }"
            + ".alert-label     { line-height: 22px; margin-top: 8px; } "
            + ".alert-textarea  { width: 76%; height: 160px; border:1px solid #AAA; padding: 2%; } "
            + ".alert-select    { padding: 4px 8px; width: 60%; } "
            + ".alert-button    { padding: 10px 20px; font-size: 14px; background-color: white; border: 1px solid #AAA; margin: 10px 10px 0 0; min-width: 100px; } "
            + ".alert-button:hover    { background-color: #EEE; } "
            + ".alert-button:focus    { background-color: #DDD; } "
            + "</style>";

var subtitle =  "<div class='header'>"
                + "<p><strong>Provide the following information about why this opportunity is being closed.</strong></p>"
                + "</div>";
        
var status =  "<div class='container'>"
            + "<div id='stdiv'><p class='alert-label'>Status</p>"
            + "<div><select class='alert-select' name='status' id='st' onchange='javascript: var s=1==this.value?document.querySelector("#srdiv").style.display=\"none\":document.querySelector(\"#srdiv\").style.display=\"inherit\";' onfocusout='javascript: statusValue = this.value;'>"
            + "<option id='Lost' value='2'>Lost</option>"
            + "<option id='Won' value='1'>Won</option>"
            + "</select></div></div>"
            + "<input type='hidden' value=" + pid + " id='op'></input>";
        
var reason1 = "<div id='srdiv'><p class='alert-label'>Status Reason</p>"
              + "<div><select class='alert-select' name='selLoseStatus' id='sr' onfocus='javascript: reasonValue = this.value;' onfocusout='javascript: reasonValue = this.value;'>";

var reason2 = "</select></div></div>";
               
var description =   "<div id='dsdiv'><p class='alert-label'>Description</p>"
                    + "<div><textarea class='alert-textarea' id='ds' onfocus='javascript: descValue = this.value;' onfocusout='javascript: descValue = this.value;' placeholder='Enter description here'></textarea></div>"
                    + "</div>"
                    + "<div id='alert-message'></div></div>";
var close1 = "<button onclick=\"javascript: "; 
                  
var clop =  "var context,st=document.querySelector('#st').value,sr=1==st?-1:document.querySelector('#sr').value,ds=document.querySelector('#ds').value,id=document.querySelector('#op').value,op=1==st?'Win':'Lose',opportunityclose={'opportunityid@odata.bind':'/opportunities('+id+')',description:ds},parameters={OpportunityClose:opportunityclose,Status:sr};context='function'==typeof GetGlobalContext?GetGlobalContext():Xrm.Page.context;var req=new XMLHttpRequest;req.open('POST',context.getClientUrl()+'/api/data/v9.1/'+op+'Opportunity',!1),req.setRequestHeader('OData-MaxVersion','4.0'),req.setRequestHeader('OData-Version','4.0'),req.setRequestHeader('Accept','application/json'),req.setRequestHeader('Content-Type','application/json; charset=utf-8'),req.onreadystatechange=function(){if(4===this.readyState)if(req.onreadystatechange=null,204===this.status)Xrm.Page.data.refresh(!0);else{var e=this.responseText;console.log(e)}},req.send(JSON.stringify(parameters));"
            + "Alert.hide();";


var close2 = "\" value='Close' class='alert-button' name='Close Opportunity' aria-label='Close Opportunity'>Close Opportunity</button>";
var notnow = "<button onclick='javascript: Alert.hide()' class='alert-button' name='Not Now' aria-label='Not Now'>Not Now</button>";                    
                    
var message = style + subtitle + status + reason1 + options + reason2 + description + notnow + close1 + clop + close2;

var buttons = [
]

var width = 480;
var height = 540;
var baseUrl = context.getClientUrl();
var preventCancel = false;
var icon = "INFO";
var padding = 30;

Alert.show(title, message, buttons, icon, width, height, baseUrl, preventCancel, padding);
}

Filed under: Blog