We're implementing a new workflow (combined with staging task sync) on an existing website where we would like to notify all members that "own" that particular section/content to approve changes.
One of the options is to have multiple roles and their corresponding workflows configured for their role and scope, but this seems like overkill - at least for us, as currently one single role is set for approvals (and another for editors)
However I've recently come across this new page property:
And have a couple of questions:
Can regular CMS users (without membership) be part of a group?
Would we be able to leverage this group for the workflow's email notifications instead of the roles? E.g. email to everyone in the owner group when a page was sent for approval.
Is this option by default inherited from the parent page when a new one is created or does it need to be set individually for each page?
We have a Kentico 11 EMS license and working on an advanced workflow, therefore custom code is possible.
Can regular CMS users (without membership) be part of a group?
- why don't you use roles here?
Would we be able to leverage this group for the workflow's email
notifications instead of the roles? E.g. email to everyone in the
owner group when a page was sent for approval.
- you'll need to customize workflow manager class, but in general yes, it is possible. You could find an inspiration in this post
Is this option by default inherited from the parent page when a new
one is created or does it need to be set individually for each page?
- Use a macro to default the field. If you populate it with anything else then the new values will be saved.
Sample code snippet for Custom Global Event Handler for Workflow steps i.e., Reject and Approve steps.
using CMS;
using CMS.Base;
using CMS.DataEngine;
using CMS.DocumentEngine;
using CMS.EmailEngine;
using CMS.EventLog;
using CMS.Helpers;
using CMS.MacroEngine;
using CMS.SiteProvider;
using CMS.WorkflowEngine;
using System;
// Registers the custom module into the system
[assembly: RegisterModule(typeof(CustomWorkflowEvent))]
public class CustomWorkflowEvent : CMSModuleLoader
{
// Module class constructor, the system registers the module under the name "CustomInit"
public CustomWorkflowEvent()
: base("CustomInit")
{
}
// Contains initialization code that is executed when the application starts
protected override void OnInit()
{
base.OnInit();
// Assigns custom handlers to events
// WorkflowEvents.Approve.After += WorkFlow_Event_After();
WorkflowEvents.Reject.After += WorkFlow_Event_After;
WorkflowEvents.Approve.After += Approve_After;
// WorkflowEvents.Action.After += WorkFlowAction_Event_After;
}
private void Approve_After(object sender, WorkflowEventArgs e)
{
try
{
WorkflowStepInfo wsi = e.PreviousStep;
if (wsi != null)
{
CMS.WorkflowEngine.Definitions.SourcePoint s = wsi.GetSourcePoint(Guid.NewGuid());
//Make sure it was an approval (standard) step
var approvers = WorkflowStepInfoProvider.GetUsersWhoCanApprove(wsi, null, SiteContext.CurrentSiteID, "UserID = " + CMSActionContext.CurrentUser.UserID, "UserID", 0, "Email, FullName, Username");
EventLogProvider.LogInformation("Approvers Data", "Approvers Data", approvers.ToString());
if (approvers != null)
{
//Loop through the approvers
string siteName = null;
SiteInfo si = SiteInfoProvider.GetSiteInfo(SiteContext.CurrentSiteID);
if (si != null)
{
siteName = si.SiteName;
}
EmailTemplateInfo eti = EmailTemplateProvider.GetEmailTemplate("Workflow.Rejected", SiteContext.CurrentSiteName);
MacroResolver mcr = MacroResolver.GetInstance();
EmailMessage message = new EmailMessage();
// Get sender from settings
message.EmailFormat = EmailFormatEnum.Both;
message.From = eti.TemplateFrom;
// Do not send the e-mail if there is no sender specified
if (message.From != "")
{
// Initialize message
// message.Recipients = strRecipientEmail;
message.Subject = eti.TemplateSubject;
// Send email via Email engine API
// EmailSender.SendEmailWithTemplateText(SiteContext.CurrentSiteName, message, eti, mcr, true);
}
}
}
}
catch (Exception ex)
{
throw;
}
}
private void WorkFlow_Event_After(object sender, WorkflowEventArgs e)
{
try
{
WorkflowStepInfo wsi = e.PreviousStep;
if (wsi != null)
{
CMS.WorkflowEngine.Definitions.SourcePoint s = wsi.GetSourcePoint(Guid.NewGuid());
//Make sure it was an approval (standard) step
var approvers = WorkflowStepInfoProvider.GetUsersWhoCanApprove(wsi, null, SiteContext.CurrentSiteID, "UserID = " + CMSActionContext.CurrentUser.UserID, "UserID", 0, "Email, FullName, Username");
EventLogProvider.LogInformation("Approvers Data", "Approvers Data", approvers.ToString());
if (approvers != null)
{
//Loop through the approvers
string siteName = null;
SiteInfo si = SiteInfoProvider.GetSiteInfo(SiteContext.CurrentSiteID);
if (si != null)
{
siteName = si.SiteName;
}
EmailTemplateInfo eti = EmailTemplateProvider.GetEmailTemplate("Workflow.Rejected", SiteContext.CurrentSiteName);
MacroResolver mcr = MacroResolver.GetInstance();
EmailMessage message = new EmailMessage();
// Get sender from settings
message.EmailFormat = EmailFormatEnum.Both;
message.From = eti.TemplateFrom;
// Do not send the e-mail if there is no sender specified
if (message.From != "")
{
// Initialize message
// message.Recipients = strRecipientEmail;
message.Subject = eti.TemplateSubject;
// Send email via Email engine API
// EmailSender.SendEmailWithTemplateText(SiteContext.CurrentSiteName, message, eti, mcr, true);
}
}
}
}
catch (Exception ex)
{
throw;
}
}
}
Hope Helps you.
Can regular CMS users (without membership) be part of a group?
It is not part of CMS users. Groups are coming from Groups Application.
GROUP: Allows you to manage user groups. Groups are a social networking
feature enabling users to find information and communicate according
to shared interests.
Would we be able to leverage this group for the workflow's email notifications instead of the roles? E.g. email to everyone in the owner group when a page was sent for approval.
No
Is this option by default inherited from the parent page when a new one is created or does it need to be set individually for each page?
No
Related
Context I am using: office-js (retrieve rest ID of message item), java backend (using GraphClient to get the immutable ID, subscription webhook endpoint)
When I get the rest itemId of the draft item via office-js like this:
Office.context.mailbox.item.saveAsync((asyncResult) => {
if (asyncResult.error) {
//hadle
} else {
resolve(
Office.context.mailbox.convertToRestId
(
asyncResult.value,
Office.MailboxEnums.RestVersion.v1_0
)
);
}
});
I send it to the backend where I translate it to Immutable ID, via GraphClient, that I save.
Once I get a notification on my subscription endpoint (I change and save the subject of the message draft
in outlook), it is successfully paired.
Problem is when I send the draft from outlook. I get notification to the subscription enpoint, but it has a different immutable ID. I create subscriptions with Prefer header like this:
Subscription subscription = new Subscription();
subscription.changeType = "updated";
subscription.notificationUrl = notificationUrl;
subscription.resource = resource;
subscription.expirationDateTime = OffsetDateTime.now().plusDays(2);
subscription.clientState = secret;
subscription.latestSupportedTlsVersion = "v1_2";
SubscriptionCollectionRequest request = graphServiceClient.subscriptions().buildRequest();
if(request != null) {
request.addHeader("Prefer", "IdType=\"ImmutableId\"");
request.post(subscription);
} else {
Is there anything I am doing wrong? Draft is move to the "Sent items" folder, which should not change immutable ID (https://learn.microsoft.com/en-us/graph/outlook-immutable-id).
Ids looks like this AAkALgAAA.........yACqAC-EWg0AC.......7B4s_RdwAA....TwAA I suppose they are correct. Just last section after underscore changes on draft sent.
Not surprising at all - it is a physically different message. Just the way Exchange works - sent/unsent flag cannot be flipped after the message is saved, so a new message is created in the Sent Items folder.
Hoping someone on here can help me out of a conundrum.
We are trying to remove all Admin sessions from our application, but are stuck with a few due to JCR Access Denied exceptions. Specifically, when we try to create AEM groups or users with a service user we get an Access Denied exception. Here is a piece of code written to isolate the problem:
private void testUserCreation2() {
String groupName = "TestingGroup1";
Session session = null;
ResourceResolver resourceResolver = null;
String createdGroupName = null;
try {
Map<String, Object> param = new HashMap<String, Object>();
param.put(ResourceResolverFactory.SUBSERVICE, "userManagementService");
resourceResolver = resourceResolverFactory.getServiceResourceResolver(param);
session = resourceResolver.adaptTo(Session.class);
// Create UserManager Object
final UserManager userManager = AccessControlUtil.getUserManager(session);
// Create a Group
LOGGER.info("Attempting to create group: "+groupName+" with user "+session.getUserID());
if (userManager.getAuthorizable(groupName) == null) {
Group createdGroup = userManager.createGroup(new Principal() {
#Override
public String getName() {
return groupName;
}
}, "/home/groups/testing");
createdGroupName = createdGroup.getPath();
session.save();
LOGGER.info("Group successfully created: "+createdGroupName);
} else {
LOGGER.info("Group already exists");
}
} catch (Exception e) {
LOGGER.error("Error while attempting to create group.",e);
} finally {
if (session != null && session.isLive()) {
session.logout();
}
if (resourceResolver != null)
resourceResolver.close();
}
}
Notice that I'm using a subservice name titled userManagementService, which maps to a user titled fwi-admin-user. Since fwi-admin-user is a service user, I cannot add it to the administrators group (This seems to be a design limitation on AEM). However, I have confirmed that the user has full permissions to the entire repository via the useradmin UI.
Unfortunately, I still get the following error when I invoke this code:
2020-06-22 17:46:56.017 INFO
[za.co.someplace.forms.core.servlets.IntegrationTestServlet]
Attempting to create group: TestingGroup1 with user fwi-admin-user
2020-06-22 17:46:56.025 ERROR
[za.co.someplace.forms.core.servlets.IntegrationTestServlet] Error
while attempting to create group. javax.jcr.AccessDeniedException:
OakAccess0000: Access denied at
org.apache.jackrabbit.oak.api.CommitFailedException.asRepositoryException(CommitFailedException.java:231)
at
org.apache.jackrabbit.oak.api.CommitFailedException.asRepositoryException(CommitFailedException.java:212)
at
org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate.newRepositoryException(SessionDelegate.java:670)
at
org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate.save(SessionDelegate.java:496)
Is this an AEM bug, or am I doing something wrong here?
Thanks in advance
So it seems the bug is actually in the old useradmin interface. It was not allowing me to add my system user into the admninistrators group, but this is possible in the new touch UI admin interface.
I am using Identity Server 3. I have couple applications ie. Client configured and have few users configured. How do i establish the relationship between User and a Client and also view all applications that the selected User has access to.
Update 1
I am sorry if question was confusing. On IdSvr3 home page, there is a link to revoke application permissions. I am guessing in order to revoke the permission you have to first establish the relationship between user and application.
and i wanted to know how to establish that permission when i add new user?
There's no direct way to limit one or multiple users to a certain client. This is where you should think about implementing your own custom validation. Fortunately, the IdentityServer provides an extensibility point for this kind of requirement.
ICustomRequestValidator
You should implement this interface to further validate users to see if they belong to certain clients and filter them out. You can look into the user details by looking at ValidatedAuthorizeRequest.Subject. This custom validator will start after validating optional parameters such as nonce, prompt, arc_values ( AuthenticationContextReference ), login_hint, and etc. The endpoint is AuthorizeEndPointController and the default implementation of the interface for the tailored job is AuthorizeRequestValidator and its RunValidationAsync. You should take a look at the controller and the class.
Implementation tip
By the time the custom request validation begins, a Client reference will be presented in ValidatedAuthorizeRequest. So all you need to do would be matching the client id or some other identifiers you think you need to verify the client. Probably, you might want to add a Claim key-value pair to your client which you want to allow a few users.
Maybe something like this.
new InMemoryUser{Subject = "870805", Username = "damon", Password = "damon",
Claims = new Claim[]
{
new Claim(Constants.ClaimTypes.Name, "Damon Jeong"),
new Claim(Constants.ClaimTypes.Email, "dmjeong#email.com"),
new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
}
}
Assume you have above user, then add the subject id to the claim of a client like below.
new Client
{
ClientName = "WPF WebView Client Sample",
ClientId = "wpf.webview.client",
Flow = Flows.Implicit,
.
.
.
// Add claim for limiting this client to certain users.
// Since a claim only accepts type and value as string,
// You can add a list of subject id by comma separated values
// eg ( new Claim("BelongsToThisUser", "870805, 870806, 870807") )
Claims = new List<Claim>
{
new Claim("BelongsToThisUser", "870805")
}
},
And then just implement the ICustomRequestValidator and try to match the Claim value with the given user in its ValidateAuthorizeRequestAsync.
public class UserRequestLimitor : ICustomRequestValidator
{
public Task<AuthorizeRequestValidationResult> ValidateAuthorizeRequestAsync(ValidatedAuthorizeRequest request)
{
var clientClaim = request.Client.claims.Where(x => x.Type == "BelongsToThisUser").FirstOrDefault();
// Check is this client has "BelongsToThisUser" claim.
if(clientClaim != null)
{
var subClaim = request.Subject.Claims.Where(x => x.Type == "sub").FirstOrDefault() ?? new Claim(string.Empty, string.Empty);
if(clientClaim.Value == userClaim.Value)
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
IsError = false
});
}
else
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
ErrorDescription = "This client doesn't have an authorization to request a token for this user.",
IsError = true
});
}
}
// This client has no access controls over users.
else
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
IsError = false
});
}
}
public Task<TokenRequestValidationResult> ValidateTokenRequestAsync(ValidatedTokenRequest request)
{
// your implementation
}
}
Time to DI
You need to inject your own dependency when you configure up your IdentityServer. The authorization server uses IdentityServerServiceFactory for registering dependencies.
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<ICustomRequestValidator>(resolver => new UserRequestLimitor()));
Then Autofac; the IoC container in IdentityServer will do the rest of the DI jobs for you.
Currently using following code for calling and email features, but it is only working in Android and not working in IOS. Also, I need these features in UWP.
For call:
string phoneno = "1234567890";
Device.OpenUri(new Uri("tel:" + phoneno));
For mail:
string email = "sreejithsree139#gmail.com";
Device.OpenUri(new Uri("mailto:" + email ));
Any package available for this?
Xamarin.Essentials (Nuget) is available as a preview package and contains functionality to both open the default mail app and attach information such as the recipients, subject and the body as well as open the phone dialer with a certain number.
There is also a blog post about Xamarin.Essentials available on blog.xamarin.com.
Edit:
As for your mail issue, Xamarin.Essentials expects an array of strings as recipients so you are able to send mail to multiple people at once. Just pass a string array with one single value.
var recipients = new string[1] {"me#watercod.es"};
If you're using the overload that expects an EmailMessage instance, you are supposed to pass a List of string objects.
In that case, the following should work:
var recipients = new List<string> {"me#watercod.es"};
Updating the complete code for calling and mailing features using Xamarin.Essentials, this might help others.
For call:
try
{
PhoneDialer.Open(number);
}
catch (ArgumentNullException anEx)
{
// Number was null or white space
}
catch (FeatureNotSupportedException ex)
{
// Phone Dialer is not supported on this device.
}
catch (Exception ex)
{
// Other error has occurred.
}
For Mail:
List<string> recipients = new List<string>();
string useremail = email.Text;
recipients.Add(useremail);
try
{
var message = new EmailMessage
{
//Subject = subject,
//Body = body,
To = recipients
//Cc = ccRecipients,
//Bcc = bccRecipients
};
await Email.ComposeAsync(message);
}
catch (Exception ex)
{
Debug.WriteLine("Exception:>>"+ex);
}
Hello to make a call in UWP:
if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.ApplicationModel.Calls.PhoneCallManager"))
{
Windows.ApplicationModel.Calls.PhoneCallManager.ShowPhoneCallUI("123", "name to call");
}
To send a Text:
private async void ComposeSms(Windows.ApplicationModel.Contacts.Contact recipient,
string messageBody,
StorageFile attachmentFile,
string mimeType)
{
var chatMessage = new Windows.ApplicationModel.Chat.ChatMessage();
chatMessage.Body = messageBody;
if (attachmentFile != null)
{
var stream = Windows.Storage.Streams.RandomAccessStreamReference.CreateFromFile(attachmentFile);
var attachment = new Windows.ApplicationModel.Chat.ChatMessageAttachment(
mimeType,
stream);
chatMessage.Attachments.Add(attachment);
}
var phone = recipient.Phones.FirstOrDefault<Windows.ApplicationModel.Contacts.ContactPhone>();
if (phone != null)
{
chatMessage.Recipients.Add(phone.Number);
}
await Windows.ApplicationModel.Chat.ChatMessageManager.ShowComposeSmsMessageAsync(chatMessage);
}
as found in Microsoft documentation here: Compose SMS documentation
==> So you can make (If not already done) a shared service interface in your Xamarin app, then the implementation with these codes in your UWP app...
To send an email:
To send an email in UWP, you can refer to the Microsoft documentation too:
Send Email documentation (UWP)
Using a plugin
Else you can use a Xamarin plugin:
documentation: Xamarin cross messaging plugin
Nuget: Nuget plugin package
In our app, we are doing the phone calling with a DependencyService.
Therefore in our PCL, we have
public interface IPhoneCall
{
void Call(string number);
}
On the iOS side, the following method does the calling:
public void Call(string number)
{
if (string.IsNullOrEmpty(number))
return;
var url = new NSUrl("tel:" + number);
if (!UIApplication.SharedApplication.OpenUrl(url))
{
var av = new UIAlertView("Error",
"Your device does not support calls",
null,
Keys.Messages.BUTTON_OK,
null);
av.Show();
}
}
If don't want to wait for the Xamarin essentials that is still in pre-release as of today, you can use this open source plugin. It works on iOS, Android and UWP. There is a sample from the github documentation :
// Make Phone Call
var phoneDialer = CrossMessaging.Current.PhoneDialer;
if (phoneDialer.CanMakePhoneCall)
phoneDialer.MakePhoneCall("+27219333000");
// Send Sms
var smsMessenger = CrossMessaging.Current.SmsMessenger;
if (smsMessenger.CanSendSms)
smsMessenger.SendSms("+27213894839493", "Well hello there from Xam.Messaging.Plugin");
var emailMessenger = CrossMessaging.Current.EmailMessenger;
if (emailMessenger.CanSendEmail)
{
// Send simple e-mail to single receiver without attachments, bcc, cc etc.
emailMessenger.SendEmail("to.plugins#xamarin.com", "Xamarin Messaging Plugin", "Well hello there from Xam.Messaging.Plugin");
// Alternatively use EmailBuilder fluent interface to construct more complex e-mail with multiple recipients, bcc, attachments etc.
var email = new EmailMessageBuilder()
.To("to.plugins#xamarin.com")
.Cc("cc.plugins#xamarin.com")
.Bcc(new[] { "bcc1.plugins#xamarin.com", "bcc2.plugins#xamarin.com" })
.Subject("Xamarin Messaging Plugin")
.Body("Well hello there from Xam.Messaging.Plugin")
.Build();
emailMessenger.SendEmail(email);
}
this is what I have so far:
void xmppConnection_OnReadXml(object sender, string xml)
{
if (xml.Contains(XmlTags.PhotoOpen))
{
int startIndex = xml.IndexOf(XmlTags.PhotoOpen) + XmlTags.PhotoOpen.Length;
int length = xml.IndexOf(XmlTags.PhotoClose) - startIndex;
string photoHash = xml.Substring(startIndex, length);
}
}
I guess I can't undo the hash, but I want to the get a person's avatar/photo. How do I achieve this?
You need to handle the VCard events and responses from XMPP connection:
private void vcardToolStripMenuItem_Click(object sender, EventArgs e)
{
RosterNode node = rosterControl.SelectedItem();
if (node != null)
{
frmVcard f = new frmVcard(node.RosterItem.Jid, XmppCon);
f.Show();
}
}
The above is from the miniclient solution example from the AGSXMPP download. Note, it happens when a user request a VCARD for a user. You can initiate that request whenever you want, however.
private void VcardResult(object sender, IQ iq, object data)
{
if (InvokeRequired)
{
// Windows Forms are not Thread Safe, we need to invoke this :(
// We're not in the UI thread, so we need to call BeginInvoke
BeginInvoke(new IqCB(VcardResult), new object[] { sender, iq, data });
return;
}
if (iq.Type == IqType.result)
{
Vcard vcard = iq.Vcard;
if (vcard!=null)
{
txtFullname.Text = vcard.Fullname;
txtNickname.Text = vcard.Nickname;
txtBirthday.Text = vcard.Birthday.ToString();
txtDescription.Text = vcard.Description;
Photo photo = vcard.Photo;
if (photo != null)
picPhoto.Image = vcard.Photo.Image;
}
}
}
That is what happens when someone requests the VCARD information from XMPP and the IQ type matches the proper data. You can thenpull the photo from vcard.Photo.
You trigger the pull with:
VcardIq viq = new VcardIq(IqType.get, new Jid(jid.Bare));
con.IqGrabber.SendIq(viq, new IqCB(VcardResult), null);
The first line there is the request to the XMPP server, that the VCARD form uses to request user information.
The second line there, sets up another grabber (callback of sorts), that the form uses to wait for the information to arrive, and then parse out the necessary information. IN this case, the grabber is in a new form, so that the main application doesn't have to worry about parsing that information.
You can look at the entire source by extracting the AGSXMPP zip file to your local drive, and looking in the Samples\VS2008\miniclient folder.
You can click link:http://forum.ag-software.de/thread/192-How-to-save-vcard-data