Webform - broken link when trying to pass values - drupal-webform

I've got a custom module that takes the email address from a form and inserts a link in the email handler (via a token) that when clicked on goes to another webform and when submitted sends the reply to the original email address.
What I'm trying to do is pass the submitted values from the first form in to this link but it's not working:
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_alter() for webform.
*
* Add the custom validation handler to the 'webform-1'.
*/
function webformcustom_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ($form_id == 'webform_submission_get_offers_add_form') {
$form['#validate'][] = 'webformcustom_validator';
}
}
/**
* The custom validation handler.
*
* Get an email of a user, encrypt it and insert it
* into the body of the notification email.
* Generate a random token and store it with a user's email as a 'key'/'value'
* using State API.
* A token that is added to the link on the webform_2 as the argument,
* like this: http://drupal8.dev/form/webform-2?token=fb41566252aec769.
* This token then will be using in the hook_preprocess_HOOK() for extraction
* of a user's email.
*/
function webformcustom_validator(&$form, FormStateInterface $form_state) {
$email = $form_state->getValue('email_address');
$webform = \Drupal::entityTypeManager()
->getStorage('webform')
->load('get_offers');
$email_handler = $webform->getHandler('email');
$configuration = $email_handler->getConfiguration();
// Generate a random token.
$token = bin2hex(openssl_random_pseudo_bytes(8));
\Drupal::state()->set('webformcustom_' . $token, $email);
$url = \Drupal::urlGenerator()->generateFromRoute('<front>', [], ['absolute' => TRUE]);
$url .= 'provide-a-quote/?select_a_car=[webform-submission:values:select_a_car]&token=' . $token;
// Insert the link to the 'webform-2' with the token into the email body.
$configuration['settings']['body'] = '<a href=' . $url . '>Submit offer</a>';
$email_handler->setConfiguration($configuration);
}
/**
* Implements hook_preprocess_HOOK() for webform templates.
*
* Read the token from the url and extract related a user's email.
* Then set a user's email in to the email handler configuration.
* This enables to notify a user when the 'webform_2' will be submit.
*/
function webformcustom_preprocess_webform(&$variables) {
if ($variables['element']['#webform_id'] == 'provide_a_quote') {
$token = \Drupal::request()->query->get('token');
$email = \Drupal::state()->get('webformcustom_' . $token);
$webform = \Drupal::entityTypeManager()
->getStorage('webform')
->load('provide_a_quote');
$email_handler = $webform->getHandler('email');
$configuration = $email_handler->getConfiguration();
$configuration['settings']['to_mail'] = $email;
$email_handler->setConfiguration($configuration);
$webform->save();
}
}
What it gives me as a link on the email is a broken link:
Audi?token=01f89ea29aa5251f>The link to the webfrom-2

Related

MS Graph API returns 500 when I try to request groups

So I am developing a sharepoint webpart where I need to check if a user is in an AD group. Now here is the odd part the MS Graph query I need to you for that is:
https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true
And that is the exact Query that my WP sends out, but it returns a 500 error message. Now I thought I had permissions missing, but the Explorer says I do not need any.
Here is my GraphService that handles MS Graph:
import { MSGraphClient } from '#microsoft/sp-http';
import * as MicrosoftGraph from '#microsoft/microsoft-graph-types';
import { WebPartContext } from "#microsoft/sp-webpart-base";
/**
* The class that handles the MS Graph API calls
*/
export default class GraphService {
/**
* The MS Graph client with does the calls
*/
private _client:MSGraphClient;
/**
* Sets the client for the Graph API, needs to be called before the class can be used
* #param context The context of the WP
*/
public async setClient(context:WebPartContext) {
this._client = await context.msGraphClientFactory.getClient();
}
/**
* Checks to see if a user belongs in a user group or not
* #param groupName The group name with we want to check
*/
public async checkCurrentUserGroup(groupName:string) {
const rawData = await this._client.api('/me/transitiveMemberOf/microsoft.graph.group').count(true).get();
const arr = rawData.value.filter(i => i.displayName == groupName);
return !!arr.length;
}
/**
* Returns the users from AD who have a manager and whom companyName is a specific company name
* #param companyName The company name to whom the users need to belong to
*/
public async getUsers(companyName:string):Promise<any[]> {
const rawData = await this._client.api('/users').filter('accountEnabled eq true').expand('manager').select('displayName,companyName,mail,department,jobTitle').get();
//need to do manual filtering, becuase you can't user filter on the "companyName" field and you can't search and expand at the same time without returing everything
const filteredData = rawData.value.filter(i => i.companyName == companyName && i.manager);
//remaps the data to IUserModel
const ret:any[] = filteredData.map(i => <any>{
name: i.displayName,
id: 0,
email: i.mail,
manager: i.manager.displayName,
managerEmail: i.manager.mail,
managerId: 0,
hasDiscussionForm: false,
department: i.department,
position: i.jobTitle
});
return ret;
}
}
The problem then is that the method checkCurrentUserGroup returns 500.
I've given the following permissions to the wp:
Group.Read.All
Directory.Read.All
User.Read
The getUsers method works as expected.
In the MS Graph Explorer the query works just fine. What am I missing?
According to the document the request header must have ConsistencyLevel and eventual header and please make sure to pass the request header and please refer to this document for more information
let res = await client.api('/users/{id}/transitiveMemberOf/microsoft.graph.group')
.header('ConsistencyLevel','eventual')
.search('displayName:tier')
.select('displayName,id')
.orderby('displayName ')
.get();

How to add worklog to Jira Issue using REST API (NodeJS)?

I am creating an add-on which should create worklogs automatically. I am using node-js (jira-connector).
I managed to get Issues, and to create new, but I am getting the error:
UnhandledPromiseRejectionWarning: Error: Missing 'worklog' property
when I want to add worklogs to a Issue
function updateIssueInJira()
{
var jira = getoAuth();
try {
return new Promise(() => {
jira.issue.addWorkLog({
opts: {
issueKey: 'NTS-4',
adjustEstimate: 'new',
newEstimade: '2d',
worklog: 'Testing'
}
})
});
} catch (error) {
console.log(error);
}
}
The definition of addWorkLog is:
/**
* Adds a new worklog entry to an issue.
*
* #method addWorkLog
* #memberOf IssueClient#
* #param {Object} opts The options to pass to the API. Note that this object must contain EITHER an issueId or
* issueKey property; issueId will be used over issueKey if both are present.
* #param {string} [opts.issueId] The id of the issue. EX: 10002
* #param {string} [opts.issueKey] The Key of the issue. EX: JWR-3
* #param {string} [opts.adjustEstimate] Allows you to provide specific instructions to update the remaining time
* estimate of the issue. Valid values are
* * "new" - sets the estimate to a specific value
* * "leave"- leaves the estimate as is
* * "manual" - specify a specific amount to increase remaining estimate by
* * "auto"- Default option. Will automatically adjust the value based on the
* new timeSpent specified on the worklog
* #param {string} [opts.newEstimate] (required when "new" is selected for adjustEstimate) the new value for the
* remaining estimate field. e.g. "2d"
* #param {string} [opts.reduceBy] (required when "manual" is selected for adjustEstimate) the amount to reduce the
* remaining estimate by e.g. "2d"
* #param {Object} opts.worklog See {#link: https://docs.atlassian.com/jira/REST/latest/#d2e1106}
* #param [callback] Called after the worklog is added.
* #return {P
romise} Resolved after the worklog is added.
*/
this.addWorkLog = function (opts, callback) {
if (!opts.worklog) {
throw new Error(errorStrings.NO_WORKLOG_ERROR);
}
var options = this.buildRequestOptions(opts, '/worklog', 'POST', opts.worklog, {
newEstimate: opts.newEstimate,
reduceBy: opts.reduceBy,
adjustEstimate: opts.adjustEstimate
});
return this.jiraClient.makeRequest(options, callback, 'Worklog Added');
};
Under getoAuth() I am doing my oAuth authentification, and I want to add the worklog to the Issue NTS-4.
I found a solution. I am sending the input directly via REST Call (POST). For that, I am using 'request'
function updateIssueInJira(oauth)
{
var testjson = {
author: {
emailAddress: "myemail"
},
comment: "Test",
timeSpentSeconds: "2700"
}
request.post({
url:'https://thelocation.com/rest/api/2/issue/ISSUEKEY/worklog/',
oauth:oauth,
json:true,
body: testjson
},
function (e, r, user) {
console.log(user)
});
}

Netsuite: how to add a custom link to the Nav Bar or Header

Is there any way to customize the Nav Bar or the Header to have a custom link?
The use-case is that I have a JIRA issue collector that is driven by javascript. I would like the user to provide feedback from the page they are having issues. However, any solution I can come up with so far takes the user away from the current page.
Example of what I have that takes the user away:
I currently have a Suitelet that is in one of the menus. That Suitelet invokes javascript but even then the user is taken away.
I have a workflow on the case record that calls some Javascript Javascript in one of the UI-based action's conditions is invoked. Similar to #1 but on the case record.
I'm thinking I'm going to need to create and public a chrome extension for my company's domain just to get a pervasive bit of javascript to run for all pages...seems like a sledgehammer.
I hope someone can prove me wrong, but as far as I am aware there is no way to natively inject Javascript or anything into the NetSuite header/navbar - they don't offer customisation to the header/navbar.
I've resorted to creating a Userscript that I load through the Violent Monkey extension for Chrome or Firefox.
Example Userscript Template
// ==UserScript==
// #name NetSuite Mods (Example)
// #namespace Violentmonkey Scripts
// #match *.netsuite.com/*
// #include *.netsuite.com/*
// #grant GM_addStyle
// #version 1.0
// #author Kane Shaw - https://stackoverflow.com/users/4561907/kane-shaw
// #description 6/11/2020, 6:25:20 PM
// ==/UserScript==
// Get access to some commonly used NLAPI functions without having to use "unsafeWindow.nlapi..." in our code
// You can add more of these if you need access to more of the functions contained on the NetSuite page
nlapiSetFieldText = unsafeWindow.nlapiSetFieldText;
nlapiSetFieldValue = unsafeWindow.nlapiSetFieldValue;
nlapiGetFieldText = unsafeWindow.nlapiGetFieldText;
nlapiGetFieldValue = unsafeWindow.nlapiGetFieldValue;
nlapiSearchRecord = unsafeWindow.nlapiSearchRecord;
nlobjSearchFilter = unsafeWindow.nlobjSearchFilter;
nlapiLookupField = unsafeWindow.nlapiLookupField;
nlapiLoadRecord = unsafeWindow.nlapiLoadRecord;
nlapiSubmitRecord = unsafeWindow.nlapiSubmitRecord;
GM_pageTransformations = {};
/**
* The entrypoint for our userscript
*/
function GM_main(jQuery) {
// We want to execute these on every NetSuite page
GM_pageTransformations.header();
GM_pageTransformations.browsertitle();
// Here we build a function name from the path (page being accessed on the NetSuite domain)
var path = location.pathname;
if(path.indexOf('.')>-1) path = path.substr(0,path.indexOf('.'));
path = toCamelCase(path,'/');
// Now we check if a page "GM_pageTransformations" function exists with a matching name
if(GM_pageTransformations[path]) {
console.log('Executing GM_pageTransformations for '+path);
GM_pageTransformations[path]();
} else {
console.log('No GM_pageTransformations for '+path);
}
}
/**
* Changes the header on all pages
*/
GM_pageTransformations['header'] = function() {
// For example, lets make the header background red
GM_addStyle('#ns_header, #ns_header * { background: red !important; }');
}
/**
* Provides useful browser/tab titles for each NetSuite page
*/
GM_pageTransformations['browsertitle'] = function() {
var title = jQuery('.uir-page-title-secondline').text().trim();
var title2 = jQuery('.uir-page-title-firstline').text().trim();
var title3 = jQuery('.ns-dashboard-detail-name').text().trim();
if(title != '') {
document.title = title+(title2 ? ': '+title2 : '')+(title3 ? ': '+title3 : '');
} else if(title2 != '') {
document.title = title2+(title3 ? ': '+title3 : '');
} else if(title3 != '') {
document.title = title3;
}
}
/**
* Changes app center card pages (dashboard pages)
*/
GM_pageTransformations['appCenterCard'] = function() {
// For example, lets make add a new heading text on all Dashboard pages
jQuery('#ns-dashboard-page').prepend('<h1>My New Dashboard Title</h1>');
}
/**
* Convert a given string into camelCase, or CamelCase
* #param {String} string - The input stirng
* #param {String} delimter - The delimiter that seperates the words in the input string (default " ")
* #param {Boolean} capitalizeFirstWord - Wheater or not to capitalize the first word (default false)
*/
function toCamelCase(string, delimiter, capitalizeFirstWord) {
if(!delimiter) delimiter = ' ';
var pieces = string.split(delimiter);
string = '';
for (var i=0; i<pieces.length; i++) {
if(pieces[i].length == 0) continue;
string += pieces[i].charAt(0).toUpperCase() + pieces[i].slice(1);
}
if(!capitalizeFirstWord) string= string.charAt(0).toLowerCase()+string.slice(1);
return string;
}
// ===============
// CREDIT FOR JQUERY INCLUSION CODE: Brock Adams # https://stackoverflow.com/a/12751531/4561907
/**
* Check if we already have a local copy of jQuery, or if we need to fetch it from a 3rd-party server
*/
if (typeof GM_info !== "undefined") {
console.log("Running with local copy of jQuery!");
GM_main(jQuery);
}
else {
console.log ("fetching jQuery from some 3rd-party server.");
add_jQuery(GM_main, "1.9.0");
}
/**
* Add the jQuery into our page for our userscript to use
*/
function add_jQuery(callbackFn, jqVersion) {
var jqVersion = jqVersion || "1.9.0";
var D = document;
var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
var scriptNode = D.createElement ('script');
scriptNode.src = 'https://ajax.googleapis.com/ajax/libs/jquery/'
+ jqVersion
+ '/jquery.min.js'
;
scriptNode.addEventListener ("load", function () {
var scriptNode = D.createElement ("script");
scriptNode.textContent =
'var gm_jQuery = jQuery.noConflict (true);\n'
+ '(' + callbackFn.toString () + ')(gm_jQuery);'
;
targ.appendChild (scriptNode);
}, false);
targ.appendChild (scriptNode);
}
You can copy and paste that code as-is into a new Userscript and it will do the following:
Make Browser tabs/windows have useful titles (shows order numbers, customer names, vendor names etc - not just "Sales Order")
Change the header background to red (as an example)
Add a new heading to the top of all "Dashboard" pages that says "My New Dashboard Title" (as an example)

oauth_signature invalid error while using Magento Rest API with GET filters

I'm having a hard time fetching orders from Magento REST API when I use its GET filters like http://localhost/magento/api/rest/orders/?filter[1][attribute]=entity_id&filter[1][gt]=70&page=1&limit=100
It is giving a "error":[{"code":401,"message":"oauth_problem=signature_invalid"}]
When I try hitting the same API endpoint using a REST Client like Postman, I'm getting back the desired results JSON.
I suspect the square brackets in the filter query might be causing a problem in generating a Oauth signature. All the endpoints without GET filters are working fine.
I'm using the Request node module to make the GET request with the oauth headers.
Is there any fix to avoid the signaturre invalid error?
The problem was within the Request node module I was using to generate a OAuth signature.It didn't factor for square brackets in the URL. I modified the code in the module for including square brackets. Changing the OAuth signature generation method fixed it for me
Since it took me a little while to figure out how to do this I figured I would pass on what I learned. My goal was to make a single request to the Magento’s REST API to return orders with specific order statuses. Looking through the GET filters documentation page wasn’t really helpful until I saw the very last line. Here is the request that I came up with that worked:
http://magentohost.com/api/rest/orders?filter[1][attribute]=status&filter[1][in][1]=pending&filter[1][in][2]=processing
The above request will get you a list of all orders with a status of “pending” or “processing”.
Ref: http://magentoforce.com/2014/08/magento-rest-api-multiple-in-get-filters/
For anyone who is wanting the answer to this here is what worked for me:
const axios = require('axios');
const crypto = require('crypto');
exports.handler = async (event) => {
let base_uri = 'https://YOUR_URL.com'; (ANYTHING UP TO THE /PATH)
const consumer_key = '';
const token_key = '';
const consumer_secret = '';
const token_secret = '';
==> CHANGE THE QUERY PARAMS TO WHATEVER YOU HAVE <==
const queryParameters = {
'searchCriteria[filterGroups][0][filters][0][field]': 'sku',
'searchCriteria[filterGroups][0][filters][0][condition_type]': 'eq',
'searchCriteria[filterGroups][0][filters][0][value]': 'aaaaaaaaa',
};
const timestamp = Math.floor(Date.now() / 1000);
const nonce = await generateNonce();
==> ADD IN YOUR PATH, IT WILL BE APPENDED TO THE BASE URL <==
const path = 'products';
base_uri = `${base_uri}/${path}`;
const reqsign256 = hmacsign256('GET', base_uri, { ...queryParameters, oauth_consumer_key: consumer_key, oauth_nonce: nonce, oauth_signature_method: 'HMAC-SHA256', oauth_timestamp: timestamp, oauth_token: token_key, oauth_version: '1.0' }, consumer_secret, token_secret);
const config = {
method: 'get',
url: base_uri,
headers: {
Authorization: `OAuth oauth_consumer_key="${consumer_key}",oauth_token="${token_key}",oauth_signature_method="HMAC-SHA256",oauth_timestamp="${timestamp}",oauth_nonce="${nonce}",oauth_version="1.0",oauth_signature="${reqsign256}"`
},
params: queryParameters,
};
let response = {};
return await axios(config)
.then(async (response) => {
response = {
statusCode: 200,
body: JSON.stringify(response.data),
};
return response;
})
.catch((error) => {
response = {
statusCode: 200,
body: JSON.stringify(error.message),
};
return response;
});
};
/**
*
* #returns A random string of 11 characters
*/
function generateNonce() {
let nonce = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 11; i++) {
nonce += possible.charAt(Math.floor(Math.random() * possible.length));
}
return nonce;
}
/**
*
* #param key
* #param body
* #param algorithm
* #description This is generating the signature using the imported crypto package.
* #returns The generated signature;
*/
function sha(key, body, algorithm) {
return crypto.createHmac(algorithm, key).update(body).digest('base64');
}
/**
*
* #param str
* #description The rfc3986 function takes a string as input and returns its encoded representation as per the rules specified in RFC 3986
* for URI (Uniform Resource Identifier) component encoding. It does this by first using encodeURIComponent to encode the string,
* which replaces certain characters with their percent-encoded representations. It then replaces characters such as !, *, (, ), and '
* with their respective percent-encoded representations, which are specified in RFC 3986 as being reserved characters that must be percent-encoded.
* #returns returns the encoded str value
*/
function rfc3986(str) {
return encodeURIComponent(str).replace(/!/g, '%21').replace(/\*/g, '%2A').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/'/g, '%27');
}
/**
*
* #param obj
* #description The map function takes an object as input and returns an array of its key-value pairs.
* It does this by iterating through the properties of the object using a for...in loop, and for each property, it checks its value.
* If the value is an array, it pushes an array with the property key and each value of the array into the result array.
* If the value is an object, it pushes an array with a string representation of the property key concatenated with the property name in square brackets
* and the property value into the result array.
* If the value is neither an array nor an object, it pushes an array with the property key and value into the result array.
* Finally, it returns the result array.
* #returns arr
*/
function map(obj) {
let key,
val,
arr = [];
for (key in obj) {
val = obj[key];
if (Array.isArray(val)) for (let i = 0; i < val.length; i++) arr.push([key, val[i]]);
else if (typeof val === 'object') for (let prop in val) arr.push([key + '[' + prop + ']', val[prop]]);
else arr.push([key, val]);
}
return arr;
}
/**
*
* #param a
* #param b
* #description Used to sort the oauth paramters into ascending order -- this is required for oauth1 to work.
* #returns the comparison result
*/
function compare(a, b) {
return a > b ? 1 : a < b ? -1 : 0;
}
/**
*
* #param httpMethod
* #param base_uri
* #param params
* #description
* 1. First, the name and value of each parameter are encoded
* 2. The parameters are sorted by name, using ascending byte value ordering. If two or more parameters share the same name, they are sorted by their value.
* 3. The name of each parameter is concatenated to its corresponding value using an "=" character (ASCII code 61) as a separator, even if the value is empty.
* for example in this case it is assigning oauth_nonce = 'foo' or oauth_signature_method = 'HMAC-SHA256' etc
* 4. The sorted name/value pairs are concatenated together into single string by using an "&" character (ASCII code 38) as separator.
* The final output will be something like this:
* GET&https%3A%2F%2Fstaging2.ospreylondon.com%2Frest%2FV1%2Fproducts&oauth_consumer_key%3Dxxx%26oauth_nonce%3xxxoauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3Dxxx%26oauth_token%3xxx%26oauth_version%3D1.0%26 ==> There will also be any url paramaters added at the end.
* #returns base url
*/
function generateBase(httpMethod, base_uri, params) {
let normalized = map(params)
// step 1
.map(function (p) {
return [rfc3986(p[0]), rfc3986(p[1] || '')];
})
// step 2
.sort(function (a, b) {
return compare(a[0], b[0]) || compare(a[1], b[1]);
})
//step 3
.map(function (p) {
return p.join('=');
})
//step 4
.join('&');
let base = [rfc3986(httpMethod ? httpMethod.toUpperCase() : 'GET'), rfc3986(base_uri), rfc3986(normalized)].join('&');
return base;
}
/**
*
* #param httpMethod
* #param base_uri
* #param params
* #param consumer_secret
* #param token_secret
* #description This takes the paramaters passed in, creates a base uri, creates a key (consumer secret and token secret with & between them)
* #returns this then returns the result of the sha method ==> this is the signature used in the request.
*/
function hmacsign256(httpMethod, base_uri, params, consumer_secret, token_secret) {
let base = generateBase(httpMethod, base_uri, params);
let key = [consumer_secret || '', token_secret || ''].map(rfc3986).join('&');
return sha(key, base, 'sha256');
}
Here is the detailed answer...
http://magentoforce.com/2014/08/magento-rest-api-multiple-in-get-filters/

Drupal: How to differentiate between new user registration and user password update in hook_user validate operation?

I am using hook_user validate operation to validate user registration info against my business logic.
I want separate logics to run on registration and change password.
But I am unable to differentiate between the two - both pass through validation and same code is run for both.
How can I differentiate between the two inside the validate op in hook_user?
with $form_id
if ( ($form_id == 'user_profile_form' && arg(3) == NULL) {
// validation code for updating
}
elseif ($form_id == 'user_register') ) {
// validation code for registering
}
In Drupal 7 you may try something like:
/**
* Implements hook_form_FORM_ID_alter().
* Form ID: user_profile_form
*/
function foo_form_user_profile_form_alter($form, &$form_state) {
// Set a custom form validate and submit handlers.
$form['#validate'][] = 'foo_form_user_profile_form_validate';
$form['#submit'][] = 'foo_form_user_profile_form_submit';
}
/**
* Implements hook_form_FORM_ID_alter().
* Form ID: user_register_form
*/
function foo_form_user_register_form_alter($form, &$form_state) {
if ($form['#user_category'] == 'account') {
// Set a custom form validate and submit handlers.
$form['#validate'][] = 'foo_form_user_register_validate';
$form['#submit'][] = 'foo_form_user_register_submit';
}
}

Resources