Quantcast
Channel: PeopleSoft Mods
Viewing all 45 articles
Browse latest View live

POC Google Authenticator Project

$
0
0

A recent comment I received made me go back and revisit my “Implementing Google Authenticator in PeopleSoft” post where I discuss the code involved to get Google Authenticator working in PeopleSoft.  Revisiting this post made me realize that I never actually shared the source App Designer project with the community.  I would like to use this post to share the plug and play POC project that I use to enforce component-level 2FA with Google Authenticator in PeopleSoft applications.

Disclaimer: I will admit that the design and logic of parts of this project are a little sketchy.  I created this POC project on the fly a while back with the intent of revisiting it to clean it up.  I began to shift my research efforts from component-level 2FA schemes to field-level 2FA schemes and I never took the time to revisit this project.  I can certainly create a better version of this project if there is interest.

With that disclaimer out of the way, this post will contain the steps to install the project and get it up and running.  If you would like more technical details of how the project works, then check this post out.

Add psGAuth.jar to class directory in your PS_HOME on the app server (%PS_HOME%\class\psGAuth.jar) and bounce the app server with a cache clear.

Import the App Designer project into App Designer and build the project.

Import and Build Project

Login to the PIA and assign a user the provided PSM_GAUTH role.

Assign Role

If you are on 8.55+ Tools, then you can make use of event mapping to achieve the 2FA functionality.  To do this, map the provided related content service named PSM_ENFORCE_GAUTH to the Pre Build event of a component that you want enable 2FA on.

Map Event

If you are not yet on the 8.55 Tools, then you will have to customize the Pre Build event of the component that you want to enable 2FA on.  I highly recommend getting on 8.55 to avoid this type of customization, but you can paste the following code into the beginning of the component’s Pre Build event to enforce 2FA:

/* import GAuth */

import PSM_GAUTH_APP_PKG:EnforceGAuth;

/* create GAuth instance */

Local PSM_GAUTH_APP_PKG:EnforceGAuth &GAuth = create PSM_GAUTH_APP_PKG:EnforceGAuth();

/* run execute to check if 2FA is needed */

&GAuth.Execute();

For example:

Customize Component

Now when you navigate to the 2FA-enabled component as the user with the PSM_GAUTH role, then you should be prompted to do a one-time setup for Google Authenticator.  The next time you access a 2FA-enabled component, you will be prompted to input a Google Authenticator TOTP unless you possess a valid 2FA cookie (more on this below).

Important Consideration:

The code is written to issue a 2FA cookie after a successful Google Authenticator challenge. On subsequent requests to 2FA-enabled components, the code will not challenge a user for 2FA if they possess a valid 2FA cookie in their browser.  I applied this logic to the project to demonstrate a general idea of how you can go about satisfying potential usability requirements.  I strongly recommend that you evaluate this practice to determine if you want or need your system to allow for this sort of 2FA bypass mechanism.  If you decide to allow for the possession of a cookie to bypass 2FA, then please DO NOT rely on the way that it is coded in the project to achieve this functionality.

Included in this project are some objects to make the 2FA process more configurable.  This includes things like setup tables to house which PeopleSoft roles to enforce 2FA for and which IP address ranges should be trusted to bypass 2FA.  Similar to the 2FA cookie bypass mechanism I described above, I included these mechanisms to expose the idea of how make the 2FA process more accommodating to usability requirements.

If you are interesting in implementing this project feel free to download and give it a try, but please keep in mind that it is merely a proof of concept of how you can go about enforcing component-level 2FA.  Please feel free to ask any questions that you may have about this project.


Making Objects Accessible in the Branding Framework

$
0
0

The Branding Framework pages are really useful because they provide the ability to add and update branding-related objects from within the PIA. These pages provide a convenient alternative to having to open up App Designer to make simple changes to HTML, JavaScript, Images, or Style Sheets. One limitation that I have found with the Branding Framework is that you cannot use it to modify objects that were not created with the framework. For example, if I want to make a change to the PT_COMMON HTML definition, then I would I have to make this change in App Designer. It turns out that there is a delivered PeopleCode library that allows us to manage the Branding Framework objects. I would like to demonstrate how this library can be used to add an existing object to the Branding Framework so that the object can be modified from the PIA.

I will start off by creating a new HTML definition in App Designer.

New_Object

As I mentioned above, I cannot simply go to the Branding Objects page (PeopleTools > Portal > Branding > Branding Objects) in the PIA to modify the contents of this object. When I go to the Branding Objects page at this point, I am presented with no modifiable objects.

Branding_Objects

I made a simple page to get the newly created object imported into the Branding Framework. The page has an input field and a button.

Test_Page

This page will take an HTML object name in the input field and upload the object to the Branding Framework on the button click.

Input_Value

When the user inputs the HTML object name into the input field and clicks the OK button, a small piece of PeopleCode fires.

Save_Object

import PTBR_BRANDING:BrandingObj;

/* Populate the user inputted object name */
Local string &sHTMLObjName = PSM_WRK0.CONTNAME.Value;

/* Reference the BrandingObj class */
Local PTBR_BRANDING:BrandingObj &objBranding;
&objBranding = create PTBR_BRANDING:BrandingObj();

/* Call the new method with the object name, object type (H-HTML,I-Image,J-JavaScript,S-Style), user ID, and date */
&objBranding.new(&sHTMLObjName, "J", %UserId, %Datetime);

/* Call the save method to save the object to the Branding Framework */
If (&objBranding.save()) Then
 MessageBox(0, "", 0, 0, "Saved");
Else
 MessageBox(0, "", 0, 0, "Save Failed");
End-If;

This PeopleCode uses the BrandingObj class of the PTBR_BRANDING application package to import the HTML object definition as a JavaScript object in the Branding Framework. The BrandingObj class can be used in the same fashion for uploading other object types such as images or style sheets into the Branding Framework.

Now when I go back to the Branding Objects page in the PIA, I can see my newly imported JavaScript object.

New_Branding_Object

I am no longer bound to only being able to update this object’s content in App Designer.  I can now modify this object from within the PIA.

Classic UI for Administrative Users

$
0
0

A common desire among organizations that adopt the Fluid user interface (UI) is the ability to keep the Classic UI for administrative users. The loudest argument backing the need for the Classic UI for admins is that the Fluid UI does not have breadcrumbs. The response to this argument is that the adoption of Fluid is more than just mobile-enabling the application, but it also entails leveraging the new Fluid navigation paradigm which means using homepages, tiles, navigation collections, search, etc., to give users an avenue to perform the transactions that they need to perform.  A proper adoption of the Fluid UI means leveraging these tools to “create your own navigation” for not only self-service users, but admin users as well.  While I don’t necessarily have a dog in this fight, I would like to provide a proof-of-concept example on how one can go about keeping administrative users Classic in a Fluid environment.

This solution is going to involve a couple of pieces: Role-Based Branding, and Event Mapping. The Role-Based Branding is going to be used to conditionally assign a Classic branding theme for users based on their PeopleSoft roles. The Event Mapping piece is to redirect admin users from the Fluid homepage to the Classic homepage. To get a rundown of each of these functionalities, then I suggest reading Sasank’s Role Based Theme Assignment/Override post as well as his Event Mapping Framework Introduction post.

First, we can tackle the Event Mapping piece of this solution. This will entail writing a simple piece of Application Class PeopleCode to redirect admin users to the Classic homepage.

Redirect_Code

import PT_RCF:ServiceInterface;

class ClassicHomeAdmin implements PT_RCF:ServiceInterface
 method execute();
end-class;

method execute
 /+ Extends/implements PT_RCF:ServiceInterface.execute +/
 /* Take admin users to classic home */
 If IsUserInRole("PAPP_SYSTEM_ADMIN") Then
 Local string &cUrl = GenerateScriptContentURL(%Portal, %Node, Record.WEBLIB_PTBR, Field.ISCRIPT1, "FieldFormula", "IScript_StartPage");
 %Response.RedirectURL(&cUrl | "?HPTYPE=C");
 End-If;
end-method;

This code checks if the user has an admin-type role using the isUserInRole function and then redirects the user to the start page IScript. The URL parameter HPTYPE with the value of “C” will tell the start page IScript to load the Classic homepage for the user. This code will need to be mapped to the Fluid Homepage component (PT_LANDINGPAGE) using Event Mapping. Note: I am using the PAPP_SYSTEM_ADMIN role to signify an admin user.  You can use any role name(s) that you want to signify admin users in your system. Be sure to keep track of the role names that you are using here because you will need to reference them later.

To use Event Mapping to map the Application Class PeopleCode, then a Related Content Service definition must be created. Head over to the Define Related Content Service page and add a new Service by providing an arbitrary Service ID.

New_Related_Content

Populate the required fields and be sure to select Application Class for the URL Type field and input the reference to the Package, Path, and Class Name to the Application Class PeopleCode that was written in the previous step.

Related_Content_Defn

Now navigate to the Manage Related Content Service page, click on the Event Mapping tab, and select the “Map the event of the Application pages” link.

Event_Mapping

This will bring up a list of the Content References for the Portal. Be sure to check the “include hidden Crefs” checkbox and drill down to the “Fluid Home” Content Reference.  This should be found under: Fluid Structure Content > Fluid Pages > PeopleSoft Applications > Fluid Home.

Fluid_Home_CREF

Clicking the Fluid Home link will bring up the page to map a Related Content Service to the component level events of the Fluid Homepage (PT_LANDINGPAGE component).  We are going to want to select Pre Build for the Event Name, the newly created Service ID for the Service ID, and Pre Process for the Processing Sequence.  Save the Component.

Map_Event

At this point if an admin user (a user with the PAPP_SYSTEM_ADMIN role) logs in or clicks the home button, then they will be taken to the Classic homepage.

Classic_Home

However, the admin user still does not have access to the Classic style theme that has the drop-down, breadcrumb navigation. We can use the User Attribute Based Theme Assignments in the Branding Framework to enable a Classic theme for admin users.

Head over to the Assign Themes page to assign a theme for the Portal that you are logged in on. On this page, there is a section where we can override the default theme that is applied to the Portal for a given role-based audience.

Classic_Theme

Since I want a Classic theme to show for admin users, I will specify a theme assignment for the role name PAPP_SYSTEM_ADMIN.   You will want to specify the same role name(s) that you are using to identify admin users in the redirection PeopleCode. For this assignment, I specified the Classic style theme named DEFAULT_THEME_TANGERINE, but you can specify any Classic theme that you want.  You will also want to ensure that the value you specify for the Priority # field for the assignment is set so that it is the highest priority, unless you want other theme assignment to take precedence.

Theme_Assignment

A browser cache clear and web server restart/cache clear might be needed for these changes to work. Now when an admin logs in or clicks the home link, they are presented with the Classic homepage with the drop-down, breadcrumb navigation. The breadcrumb navigation header will stay with the admin user as they navigate around the system.

Classic_Theme_Home

This is all of the setup that is needed to enable admin users to use the Classic UI in a Fluid environment.

It is worth mentioning that the choice of implementing this sort of solution will most likely bear support implications as it goes against Oracle’s intended usage of the application. Organizations that want to adopt Fluid need to first consider how the delivered Fluid navigation techniques can be leveraged to support all of the navigation use cases before resorting to a custom solution.

Using PT_METADATA to Create Objects

$
0
0

I was recently poking around the delivered code behind the Branding Framework pages. These are the pages that allow us to create HTML, JavaScript, Style Sheet, and Image definitions within the PIA. These pages are interesting because they prove that app designer is not needed to create these types of objects. It turns out that there is a nice delivered library that is used in the code behind these pages to create these objects. The library is nicely bundled in an application package named PT_METADATA. This package contains all of the necessary classes to create HTML, JavaScript, Style Sheet, and Image definitions. In this post I will share some code samples of how this library can be used to create HTML and Style Sheet objects with PeopleCode.

For demonstration purposes, I created a page that functions similar to the delivered Branding Framework pages. This page will have a few input fields to specify the needed attributes to create an HTML or Style Sheet object definition.

Populated_Fields

In this example, I am creating an HTML object. There are only three required fields for HTML object definitions and they are the object name, object description, and the content the object should store. After populating the values and clicking the OK button, a small piece of PeopleCode fires.

Create_HTML

/* Create HTML Object */
import PT_METADATA:MetaDataAPI:HTMLDefn;

Local string &sHTMLObjName = PSM_WRK0.CONTNAME.Value;
Local string &sHTMLObjContent = PSM_WRK0.CONTDATA.Value;
Local string &sHTMLObjDescr = PSM_WRK0.DESCR.Value;

Local PT_METADATA:MetaDataAPI:HTMLDefn &oHTMLMetaData;
&oHTMLMetaData = create PT_METADATA:MetaDataAPI:HTMLDefn(&sHTMLObjName);

If (&oHTMLMetaData.Open()) Then
 &oHTMLMetaData.ContentData = &sHTMLObjContent;
 &oHTMLMetaData.Description = &sHTMLObjDescr;
 If (&oHTMLMetaData.Save()) Then
 MessageBox(0, "", 0, 0, "Saved");
 Else
 MessageBox(0, "", 0, 0, "Save failed");
 End-If;
Else
 Error MsgGetText(0, 0, "Open failed");
End-If;

This code is using the HTMLDefn class to create and store all of the meta-data for the HTML object definition. This is essentially the same code that fires on the Branding Objects page when you create a new object. After the code fires, the new object can be seen in App Designer.

New_Object

It is worth noting that this same code can be used to edit an existing HTML definition. So in this example, if the PSM_MY_HTML object already existed, then this code would’ve overwritten the object’s description and content with the values that were specified in the input fields.

Style Sheets can be created/updated in the same fashion that the HTML objects can. There is a separate class named StylesheetDefn that is used for Style Sheet objects.  Below is sample usage of this class.  As you can see, this class operates similar to the HTMLDefn class.

Create_Style

/* Create Style Object */
import PT_METADATA:StylesheetDefn;

Local PT_METADATA:StylesheetDefn &oSSMetaData;

Local string &sSSObjName = PSM_WRK0.CONTNAME.Value;
Local string &sSSObjContent = PSM_WRK0.CONTDATA.Value;
Local string &sSSObjDescr = PSM_WRK0.DESCR.Value;

&oSSMetaData = create PT_METADATA:StylesheetDefn(&sSSObjName);

If (&oSSMetaData.Open()) Then
 &oSSMetaData.ExtStyleSheetStr = &sSSObjContent;
 &oSSMetaData.Descr = &sSSObjDescr;
 If (&oSSMetaData.Save()) Then
 MessageBox(0, "", 0, 0, "Saved");
 Else
 MessageBox(0, "", 0, 0, "Save failed");
 End-If;
Else
 Error MsgGetText(0, 0, "Open failed");
End-If;

Other than providing some exposure to an under documented PeopleTools gem, my usage of the PT_METADATA app package to create objects in this example is not very useful as it mimics delivered functionality. However, I think there can be practical usages of this library in different contexts that can allow for enhancing the process of object/data management.

In my next post, I will provide an example of using this library to create image objects. While doing this, I will provide a technique of how this library can provide an avenue to communicate data to be cached on the web server.

PT_METADATA and Web Server Cache

$
0
0

A while back I presented a solution to manage configuration data on the web server. This solution involved a servlet-based cache managing utility that was responsible for the storage and caching of data to the PeopleSoft web server. I have been experimenting with ways that I can get custom data cached to the PeopleSoft web server without being so intrusive to the web-tier. I recently came across the PT_METADATA app package which can be used to create object types such as HTML, Style Sheet, and Images. These object types are nice because they are PeopleTools-managed and they have the flexibility to store custom data expressed in any form. Another great perk of these object types is that they can be cached to the web server using the %Response class. I am going to demonstrate a technique that I use to get PeopleTools to manage and cache my custom data.

This solution is going to involve using the PT_METADATA app package to create new image objects with PeopleCode.  For demonstration purposes, I created a custom page to gather the required fields to create an Image object.

Required_Fields

The four required fields to create an image object are the object name, object description, object file extension, and the content to store in the object. The object’s file extension is limited to three characters, but can be set to any three characters. This allows us to have non-image-type extensions such as htm, js, css, txt, dat, or log for example. Also, the data that can be stored in the image object can be of any form. We can store HTML, XML, or JSON for example.  Most importantly however, the type of data that an image object can hold is arbitrary. Image objects do not actually have to hold images/image data just like HTML objects don’t have to hold actual HTML. This is why these object types are so great because they are a Tools objects that have the flexibility to store custom data.

As you see in the picture above, I am storing some JSON data in an image object with a “dat” file extension. When I click the OK button, a small piece of PeopleCode fires to create a new image object and cache the object to the web server cache directory.

Create_Image_Object

/* Create Image Object */
import PT_METADATA:MetaDataAPI:ImageDefn;

Local string &sIMGObjName = PSM_WRK0.CONTNAME.Value;
Local string &sFileExtension = PSM_WRK0.EXTENSION.Value;
Local string &sIMGObjDescr = PSM_WRK0.DESCR.Value;
Local Field &fIMGObjContent = GetLevel0().GetRow(1).GetRecord(Record.PSM_WRK0).CONTDATA;

Local PT_METADATA:MetaDataAPI:ImageDefn &oIMGMetaData;
&oIMGMetaData = create PT_METADATA:MetaDataAPI:ImageDefn(&sIMGObjName);

If (&oIMGMetaData.Open()) Then
 &oIMGMetaData.SetContentData(&sFileExtension, &fIMGObjContent);
 &oIMGMetaData.Description = &sIMGObjDescr;
 If (&oIMGMetaData.Save()) Then
 MessageBox(0, "", 0, 0, "Saved");
 Else
 MessageBox(0, "", 0, 0, "Save failed");
 End-If;
Else
 Error MsgGetText(0, 0, "Open failed");
End-If;

/* Cache the file */
Local string &sIMGName = "IMAGE." | &sIMGObjName;
Local string &sCachedURL = %Response.GetImageURL(@&sIMGName);

/* Only show the non-versioned filename if ENABLENOVERSION is checked on the web profile */
Local string &sCachedURLNoVersion = Substring(&sCachedURL, 1, Find(&sIMGObjName, &sCachedURL) - 1) | &sIMGObjName | "." | &sFileExtension;
PSM_WRK0.HTMLAREA.Value = "<br><div class=""PSEDITBOXLABEL""><b>Cached URL: </b><a href=""" | &sCachedURL | """target=""_blank"">" | &sCachedURL | "</a></div>";
PSM_WRK0.HTMLAREA.Value = PSM_WRK0.HTMLAREA.Value | "<br><div class=""PSEDITBOXLABEL""><b>Cached URL (No Version): </b><a href=""" | &sCachedURLNoVersion | """target=""_blank"">" | &sCachedURLNoVersion | "</a></div>";

The code uses the ImageDefn class to create an image object based on the provided input values. After the image object is created, the GetImageURL method of the %Response class is invoked to get the newly created data stored to the web server’s cache directory. The cached URLs are presented to the user at this point.

Cache_URLs

The GetImageURL method will create two cached URLs for the data file.  The first one is the versioned one and the second is the non-versioned one.  Here is a look at the web server’s cache directory.

Cached_Files

The reason that there were two URLs that got generated is due to a setting on the web profile that allows for a non-versioned copy of cached Image and Style objects to be generated in the web server’s cached directory.  This setting can be toggled by checking the checkbox labeled “Copy Image/CSS (no Versioning)”under the caching tab of the web profile.

Enable_No_Version

What this setting does is it forces the web server to maintain two separate versions of cached image and css files. This allows for one version of the file to keep a static filename, while the other copy has a version number appended to the end of it. This is a great feature because the non-versioned version of the cached file is always reference-able since its filename stays the same, but its content will always be up to date. This, along with the file extension flexibility is the main reasons that I am using Image objects to house my custom data verses HTML or Style objects. Side Note: The “Copy Image/CSS (no Versioning)” functionality on the web profile did not work for me when I would cache Style objects. This is another reason that I am using Image objects with this solution.

So this is how one can go about generating data on the fly and immediately getting it cached to the web server.  I like this solution because it allows for the storage and caching to be managed completely by PeopleTools. One thing to keep in mind is the sensitivity of data that you are caching. I am using this solution to cache non-sensitive configuration data. I advise to not cache any sensitive data with this solution as the cache directory is available to any authenticated user (or non-authenticated user if the public user is enabled).

Location-Based Menu Pruning

$
0
0

Menu pruning is the process of limiting the items that show up in a menu for a user. This process is desirable in situations where you want to prevent a user from accessing certain content references. An example scenario of this could be that you don’t want to let administrators access Query Viewer when they are not coming in from a trusted location. The fashion in which the administrator’s access is limited in this scenario is the process of location-based security. With location-based security, you can let the location that a user is coming from dictate the type of access that a user has in the application. So if we put these two terms together, we get location-based menu pruning. Location-based menu pruning is the process of limiting the items that show up in the menu for a user based on the user’s location.  In this post, I will discuss how we can perform location-based menu pruning on the PeopleSoft Fluid Navigator.


I will be demonstrating various ways that we can alter the Navigator menu that gets displayed to a user at run time. This will be achieved by using Event Mapping to map custom code to the post-processing of the Navigator Component’s Post Build event. The name of the Component used for the Navigator is PTNUI_MENU_COMP. I will be sharing some code samples that will need to be mapped with Event Mapping.  If you are unsure of what Event Mapping is or how to use it, then I recommend reading this introductory Event Mapping post written by Sasank Vemana.

Before I jump into the menu pruning examples, I want to clarify one really important piece of information. Menu pruning in itself is a user interface enhancement and NOT a security enhancement. Menu pruning is just going to remove the available Navigator (menu) paths that a user has to particular content references. So if you prune out the “Query Viewer” CREF from the menu, then you are just removing the avenue that the user has to that CREF. If the user knows the URL for the “Query Viewer” CREF, then they can simply paste it in their browser and have access to the component. If you are interested in actually securing the CREFs that you are pruning from the menu, then I suggest reading this post on Enabling Location Based Security in PeopleSoft with Event Mapping.

With that out of the way, let’s take a look at some location-based menu pruning examples. The code samples provided for each of these examples is heavily commented, so I believe that it can explain itself. However, I will give a high-level overview of what each of these code samples are doing. Note: This code was tested on an Interaction Hub system running 8.55.12 PeopleTools.

Example of Deleting Menu Items Based on a User’s Location

This first example is fairly straight-forward. What is happening here is that we are getting a reference to the current NBContetCollection object from the component buffer. This object holds a collection of all of the menu items that will get displayed to the user in the Navigator. The NBContetCollection class has a DeleteItem method that will delete a menu item (CREF) from the collection, which results in the deleted CREF to not be displayed. In this example, the code first checks if the user is coming from a trusted location before calling the DeleteItem method to delete the desired CREFs.

DeleteExample

import PT_RCF:ServiceInterface;
import PTNUI:NavBarContentArea:NBContentCollection;
import PTNUI:NavBarContentArea:NBContent;

class EMF2 implements PT_RCF:ServiceInterface
 method execute();
end-class;

/* This is a reference to the current nav bar content collection */
Component PTNUI:NavBarContentArea:NBContentCollection &ptnui_CurContentColl;

/* Use this function to get a reference to the appropriate portal */
Declare Function PortalOpen PeopleCode FUNCLIB_PORTAL.PORTAL_GEN_FUNC FieldFormula;

/* Use this function to set what shows up in the content (menu) area */
Declare Function SetContentArea PeopleCode PTNUI_DOCK_REC.PTNUI_NB_ACTION FieldFormula;

/* Location-based menu pruning example. Note: You can prune the menu based on any conditions (doesn't have to be location) */
method execute
 /+ Extends/implements PT_RCF:ServiceInterface.execute +/
 
 /* Check if the user is coming from a trusted location */
 /* TODO: store trusted IP address ranges in setup table to avoid hard-coding IP addresses */
 If (All(&ptnui_CurContentColl) And
 %Request.RemoteAddr <> "192.168.56.10") Then
 
 /* Delete the PeopleTools folder CREF */
 /* TODO: Store CREFs to delete in setup table to avoid hard-coding */
 &ptnui_CurContentColl.DeleteItem("PT_PEOPLETOOLS");
 
 /* Delete the Change My Password CREF */
 &ptnui_CurContentColl.DeleteItem("PT_CHANGE_PASSWORD_GBL");
 
 /* Delete the Query Viewer CREF */
 &ptnui_CurContentColl.DeleteItem("PT_QUERY_VIEWER_GBL");
 
 
 /* Get a reference to the current portal */
 Local ApiObject &portal = PortalOpen();
 
 /* Set the content area equal to the modified nav bar content collection */
 SetContentArea(&portal, &ptnui_CurContentColl);
 
 /* The Close method closes the PortalRegistry object */
 &portal.close();
 
 End-If;
 
end-method;

 

Example of Changing the Style of Menu Items Based on a User’s Location

In this next example, we are altering the style for a given CREF that will be displayed to a user. Specifically, we are overriding the style of the PeopleTools folder by setting it to the “ps_hidden” style. This style simply hides the element that it is applied to. So the output for this example is actually the same as if we were to delete the item. However, with this example, the PeopleTools folder could be exposed by inspecting the page’s source. Applying the “ps_hidden” style is not very practical, but I just wanted to demo how one can override the style of a given menu item. A more practical use case could be to inject a custom style using the AddStyleSheet function and then override the menu items style with the custom one.

AlterStyleExample1AlterStyleExample2

import PT_RCF:ServiceInterface;
import PTNUI:NavBarContentArea:NBContentCollection;
import PTNUI:NavBarContentArea:NBContent;

class EMF2 implements PT_RCF:ServiceInterface
 method execute();
end-class;

/* This is a reference to the current nav bar content collection */
Component PTNUI:NavBarContentArea:NBContentCollection &ptnui_CurContentColl;

/* Use this function to get a reference to the appropriate portal */
Declare Function PortalOpen PeopleCode FUNCLIB_PORTAL.PORTAL_GEN_FUNC FieldFormula;

/* Use this function to set what shows up in the content (menu) area */
Declare Function SetContentArea PeopleCode PTNUI_DOCK_REC.PTNUI_NB_ACTION FieldFormula;

/* Location-based menu pruning example. Note: You can prune the menu based on any conditions (doesn't have to be location) */
method execute
 /+ Extends/implements PT_RCF:ServiceInterface.execute +/
 
 /* Check if the user is coming from a trusted location */
 If (All(&ptnui_CurContentColl) And
 %Request.RemoteAddr <> "192.168.56.10") Then
 
 /* Create a Nav Bar Content Collection variable to store the altered menu */
 Local PTNUI:NavBarContentArea:NBContentCollection &ptnui_CustomContentColl = create PTNUI:NavBarContentArea:NBContentCollection();
 
 /* This will hold the current menu item CREF */
 Local PTNUI:NavBarContentArea:NBContent &oCurrentMenuItem;
 
 /* Start off by setting the current menu item CREF to the first menu item CREF in the collection */
 &oCurrentMenuItem = &ptnui_CurContentColl.First();
 
 /* Loop through all of the menu item CREFs in the collection */
 While (All(&oCurrentMenuItem))
 
 /* Apply the the "ps_hidden" style to the PeopleTools folder CREF */
 If (&oCurrentMenuItem.getName() = "PT_PEOPLETOOLS") Then
 
 /* Set the style of the menu item as you please */
 &oCurrentMenuItem.style = "ps_hidden";
 
 End-If;
 
 /* Insert the current menu item CREF into the modified menu content collection */
 &ptnui_CustomContentColl.InsertItem(&oCurrentMenuItem);
 
 /* Set the current menu item CREF to the next menu item CREF in the collection */
 &oCurrentMenuItem = &ptnui_CurContentColl.Next();
 
 End-While;
 
 /* Set the component Nav Bar Content Collection variable to the modified version */
 &ptnui_CurContentColl = &ptnui_CustomContentColl;
 
 /* Get a reference to the current portal */
 Local ApiObject &portal = PortalOpen();
 
 /* Set the content area equal to the modified nav bar content collection */
 SetContentArea(&portal, &ptnui_CurContentColl);
 
 /* The Close method closes the PortalRegistry object */
 &portal.close();
 
 End-If;
 
end-method;

 

Example of Changing the Action of Menu Items Based on a User’s Location

In this last example, we are overriding the action that occurs when a user clicks on a particular CREF. Specifically, what we have done is overridden the location that a user is taken to when the user clicks the “Change My Password” menu item. In this example, the user will be taken to the http://www.peoplesoftinfo.com address when they click the “Change My Password” menu item. I believe there can be many practical uses of this functionality.

AlterActionExample1AlterActionExample2

import PT_RCF:ServiceInterface;
import PTNUI:NavBarContentArea:NBContentCollection;
import PTNUI:NavBarContentArea:NBContent;

class EMF2 implements PT_RCF:ServiceInterface
 method execute();
end-class;

/* This is a reference to the current nav bar content collection */
Component PTNUI:NavBarContentArea:NBContentCollection &ptnui_CurContentColl;

/* Use this function to get a reference to the appropriate portal */
Declare Function PortalOpen PeopleCode FUNCLIB_PORTAL.PORTAL_GEN_FUNC FieldFormula;

/* Use this function to set what shows up in the content (menu) area */
Declare Function SetContentArea PeopleCode PTNUI_DOCK_REC.PTNUI_NB_ACTION FieldFormula;

/* Location-based menu pruning example. Note: You can prune the menu based on any conditions (doesn't have to be location) */
method execute
 /+ Extends/implements PT_RCF:ServiceInterface.execute +/
 
 /* Check if the user is coming from a trusted location */
 If (All(&ptnui_CurContentColl) And
 %Request.RemoteAddr <> "192.168.56.10") Then
 
 /* Create a Nav Bar Content Collection variable to store the altered menu */
 Local PTNUI:NavBarContentArea:NBContentCollection &ptnui_CustomContentColl = create PTNUI:NavBarContentArea:NBContentCollection();
 
 /* This will hold the current menu item CREF */
 Local PTNUI:NavBarContentArea:NBContent &oCurrentMenuItem;
 
 /* Start off by setting the current menu item CREF to the first menu item CREF in the collection */
 &oCurrentMenuItem = &ptnui_CurContentColl.First();
 
 /* Loop through all of the menu item CREFs in the collection */
 While (All(&oCurrentMenuItem))
 
 /* Alter the click action of the Change My Password CREF */
 If (&oCurrentMenuItem.getName() = "PT_CHANGE_PASSWORD_GBL") Then
 
 /* Set the action URL to open the PeopleSoftInfo website */
 Local string &sUrl = "javascript:PTNavBar.OpenInWindow('" | "http://www.peoplesoftinfo.com" | "');";
 
 /* Create a modified version of the current menu item that will open the PeopleSoftInfo website when clicked */
 Local PTNUI:NavBarContentArea:NBContent &oModifiedMenuItem = create PTNUI:NavBarContentArea:NBContent(&oCurrentMenuItem.getName(), &oCurrentMenuItem.getLabel(), &sUrl);
 &oModifiedMenuItem.style = &oCurrentMenuItem.style;
 &oModifiedMenuItem.ariaAttributes = &oCurrentMenuItem.ariaAttributes;
 &oCurrentMenuItem = &oModifiedMenuItem;
 
 End-If;
 
 /* Insert the current menu item CREF into the modified menu content collection */
 &ptnui_CustomContentColl.InsertItem(&oCurrentMenuItem);
 
 /* Set the current menu item CREF to the next menu item CREF in the collection */
 &oCurrentMenuItem = &ptnui_CurContentColl.Next();
 
 End-While;
 
 /* Set the component Nav Bar Content Collection variable to the modified version */
 &ptnui_CurContentColl = &ptnui_CustomContentColl;
 
 /* Get a reference to the current portal */
 Local ApiObject &portal = PortalOpen();
 
 /* Set the content area equal to the modified nav bar content collection */
 SetContentArea(&portal, &ptnui_CurContentColl);
 
 /* The Close method closes the PortalRegistry object */
 &portal.close();
 
 End-If;
 
end-method;

 


 

These are a few ways that we can dynamically change the look and feel of the Navigator. In these examples, the alterations of the menu items were conditioned upon the user’s location. It should be clear that you can alter the menu items based on any condition(s) and not just location.

While I think this solution has potential to be powerful, the effectiveness of it can vary depending on the navigation implementation for a given PeopleSoft system. What I mean by this is that the Navigator menu may not be the only avenue to a given CREF. PeopleSoft delivers many other navigation utilities along with the Navigator. I would like it to be well understood that the solutions that I have demonstrated today is just for altering the behavior of the Navigator, and not the other delivered navigation techniques/utilities.

Understanding the %metadata Application Package

$
0
0

The %metadata application package in PeopleSoft is very intriguing.  This app package is unlike any other as we (PeopleSoft Developers) do not have access to the implementation of this package.  When you try to open this package in App Designer, the IDE acts as if the package does not exist.  However, if you correctly reference this package’s contents (sub-packages, classes, methods, properties, etc.) then App Designer does not bat an eye.  It should be well understood that (our) usage of this package is not supported by Oracle as it is undocumented.  However, there are currently no measures in place to prevent blind usage this app package.  While I definitely do not advise the usage of this package in any legitimate PeopleSoft system, I thought it would be a fun educational exercise to try to understand this mysterious app package.

I would like to start this off by making it clear that none of the information I am presenting in this post should be treated as factual.  This is merely my understanding an obscure and undocumented PeopleTools technology.

References to the %metadata package can be seen in some of the delivered code.  From what I can gather, the delivered code uses this package for accessing/manipulating PeopleTools-managed objects.  This package offers a (potentially safer) alternative to accessing/manipulating PeopleTools objects through the metadata tables in the database.

 

Overview of %metadata

There are three major aspects to understand when using the %metadata package.

  • Definitions
  • Managers
  • Keys

Definitions can be thought of as the object types.  So if you want to work with a record object type, then it would be referenced with %metadata as follows:

/* Record Definition */
Local %metadata:RecordDefn:RecordDefn &oRecordDefn;

In order to obtain a reference to an object definition, you must use the manager for the given object type. Managers are responsible for fetching  and creating object definitions.  So to work with a record definition, the record definition manager must first be instantiated as follows:

 /* Record Manager */
 Local %metadata:RecordDefn:RecordDefn_Manager &oRecordDefn_Manager;
 &oRecordDefn_Manager = create %metadata:RecordDefn:RecordDefn_Manager();

In order for the manager to supply a reference to an existing object definition, you will have to supply the manager with the key to the definition.  Keys are name-value pairs that reference a single definition of a given object type.  So if we want to manage a record named “PSM_TEST”, then the key would be created as follows:

 /* Example Key Object representing a record */
 Local %metadata:Key &oRecordKey;
 &oRecordKey = create %metadata:Key(Key:Class_Record, "PSM_TEST");

The key object instantiation is rather strange (syntax-wise) and I will get into more detail on this later.  But for now, we can feed the generated key object to the record manager’s GetDefn method, to obtain a non-updateable reference to the PSM_TEST record object definition.

/* Get reference to a non-updateable record definition */
&oRecordDefn = &oRecordDefn_Manager.GetDefn(&oRecordKey);

If you want to make changes to the record definition, then you can obtain an updatable version of the definition by supplying the generated key to the manager’s GetDefnToUpdate method.

/* Get reference to an updatable record definition */
&oRecordDefn = &oRecordDefn_Manager.GetDefnToUpdate(&oRecordKey);

As I said before, managers are capable of creating new object definitions.  The key is not needed for creating a new object.  The manager’s CreateDefn method will return a reference to a new (empty) record definition.

/* Get reference to a new record definition */
&oRecordDefn = &oRecordDefn_Manager.CreateDefn();

Now that we have a high level overview of the important pieces to using %metadata, I would like to go into more granular detail of each of the pieces so that we can understand how to actually manipulate PeopleTools-managed objects.

 

Understanding %metadata Definitions

%metadata has numerous sub-packages that  each represent a PeopleTools-managed object type.  I gave an example of accessing the Record object type above by referencing the RecordDefn sub-package. Many (if not all) object types can be referenced with %metadata and they are all referenced by a somewhat guessable sub-package naming scheme.  Here are some examples:

 /* Example Definition Objects */
 Local %metadata:MenuDefn:MenuDefn &oMenuDefn;
 Local %metadata:ComponentDefn:ComponentDefn &oComponentDefn;
 Local %metadata:PageDefn:PageDefn &oPageDefn;
 Local %metadata:PeopleCodeProgram:PeopleCodeProgram &oPeopleCodeProgram;
 Local %metadata:AppPackageDefn:AppPackageDefn &oAppPackageDefn;
 Local %metadata:RoleDefn:RoleDefn &oRoleDefn;
 Local %metadata:PermissionListDefn:PermissionListDefn &oPermissionListDefn;

The key takeaway to understanding these objects is that they essentially represent their database “*DEFN” table counterpart.  For example,  in order to understand the %metadata Record Definition, you must first understand the structure of the PSRECDEFN table in the database.  The properties of the Record Definition object should, more or less, reflect the fields that are on the PSRECDEFN table.

 

Understanding %metadata Managers

I have found that each %metadata object definition package contains a manager class.  The exact name of the manger class for any given object definition seems to be the object definition name with “_Manager” appended to it.   As we saw earlier the manger class name for the RecordDefn Record object definition was RecordDefn_Manager.  Here are some examples of other types of managers:

 /* Example Manager Objects */
 Local %metadata:MenuDefn:MenuDefn_Manager &oMenuDefn_Manager;
 Local %metadata:ComponentDefn:ComponentDefn_Manager &oComponentDefn_Manager;
 Local %metadata:PageDefn:PageDefn_Manager &oPageDefn_Manager;
 Local %metadata:PeopleCodeProgram:PeopleCodeProgram_Manager &oPeopleCodeProgram_Manager;
 Local %metadata:AppPackageDefn:AppPackageDefn_Manager &oAppPackageDefn_Manager;
 Local %metadata:RoleDefn:RoleDefn_Manager &oRoleDefn_Manager;
 Local %metadata:PermissionListDefn:PermissionListDefn_Manager &oPermissionListDefn_Manager;

All of the manager classes seem to have a common set of methods for getting and creating their respective object definitions.  I demonstrated earlier the GetDefn, GetDefnToUpdate, and CreateDefn methods for the Record Definition manager class.  I have found that these methods are defined in manager classes of other object definitions as well.  Along with these three methods are a coulple of other commom methods: DefnExists and GetPrivateDefn.  I am unsure of use cases for the GetPrivateDefn method, but the DefnExists method can be use to determine if an object defnition exists for a provided %metadata:key.

 

Understanding %metadata Keys

The keys are used to reference a specific definition for a given object type.  The key object is a required parameter for all manager class’s GetDefn, GetDefnToUpdate, and DefnExists mehods. The instantiation of a key object is rather strange as the key constructor behaves in an overloaded fashion and takes a non-PeopleCode object type as a parameter.  This can be proven by trying to extend the Key object’s constructor in App Designer:

Key_Constructor

As you can see from the picture above, the key’c constructor takes a RepeatedAny object type, which is not a native PeopleCode object type.  This leads me to believe that we are directly referencing a lower-level language (non-PeopleCode) implementation of these (%metadata) objects.  This could be one of the reasons why we do not have access to view the source of the %metadata package in App Designer.

Aside from its odd syntactical references, the key object is fairly straight-forward to understand. The %metadata:key class contains numerous sub-class, integer constants.  Each of the integer constants represent a specific object type. As we saw earlier, I used the Class_Record sub-class to represent the Record Definition object type.  Here are some examples of how to reference other available key sub-classes:

 /* Example Key sub-class, integer constants */
 Local integer &iMenuObjType = Key:Class_Menu;
 Local integer &iComponentObjType = Key:Class_PanelGroup;
 Local integer &iMarketObjType = Key:Class_Market;
 Local integer &iPageObjType = Key:Class_Panel;
 Local integer &iMethodObjType = Key:Class_Method;

The integers returned by these sub-classes are the key in the key-value pairs to construct a %metadata:key to give to a manager for an object definition.  The value in the key-value pair is simply a string.   Earlier I inputted the string “PSM_TEST” to refer to a record definition of a record named PSM_TEST.  To further solidify our understanding of this concept, I would like to provide another example of instantiating a key.  This time, I want to refer to a Component-level PeopleCode Program object. Specifically,  I will reference the PostBuild event to the USERMAINT Component:

 /* Example Key Object representing a PeopleCode Program */
 Local %metadata:Key &oKey = create %metadata:Key(Key:Class_PanelGroup, "USERMAINT", Key:Class_Market, "GBL", Key:Class_Method, "PostBuild");

A similar way to generate keys is to use the AddItem method.  This is my preferred way to generate keys as it is a bit easier to read and understand.  Here is an example of using AddItem to generate a key that references the same Component-level PeopleCode Program object as in the previous example:

 /* Instantiate the Key object */
 Local %metadata:Key &oKey = create %metadata:Key();
 
 /* Add the Component key-value pair */
 &oKey.AddItem(Key:Class_PanelGroup, "USERMAINT");
 
 /* Add the Market key-value pair */
 &oKey.AddItem(Key:Class_Market, "GBL");
 
 /* Add the Event key-value pair */
 &oKey.AddItem(Key:Class_Method, "PostBuild");

The knowledge of which key-value pairs are needed to reference a particular object type can be derived a couple of different ways.  The first way is to consider the key fields of the corresponding “*DEFN” database table of the object type that you are referencing.  The second way is to simply think of what input values App Designer requires for you to view a particular object type.

 

A Complete %metadata Example

I would now like to provide a complete example of using the %metadata application package. In this example, I will be editing the PeopleCode defined in the PostBuild event of the USERMAINT component.

import %metadata:Key;
import %metadata:PeopleCodeProgram:PeopleCodeProgram_Manager;
import %metadata:PeopleCodeProgram:PeopleCodeProgram;

/* Instantiate the Key object */
Local %metadata:Key &oKey = create %metadata:Key();

/* Add the USERMAINT Component key */
&oKey.AddItem(Key:Class_PanelGroup, "USERMAINT");

/* Add the GBL Market key */
&oKey.AddItem(Key:Class_Market, "GBL");

/* Add the PostBuild Event Name key */
&oKey.AddItem(Key:Class_Method, "PostBuild");

/* Instantiate the PeopleCode Program Manager object */
Local %metadata:PeopleCodeProgram:PeopleCodeProgram_Manager &oManager = create %metadata:PeopleCodeProgram:PeopleCodeProgram_Manager();

/* Determine if a PeopleCode Program Definition exists for the given key */
Local boolean &bExists = &oManager.DefnExists(&oKey);

/* Throw an exception if the definiton does not exists */
If Not (&bExists) Then
 throw CreateException(0, 0, "Definition does not exist for the provided key");
End-If;

/* Get the PeopleCode Program Definition */
Local %metadata:PeopleCodeProgram:PeopleCodeProgram &oPeopleCodeProgram = &oManager.GetDefnToUpdate(&oKey);

/* Get the PeopleCode that is definied for the loaded PeopleCode Program */
Local string &sProgram = &oPeopleCodeProgram.GetProgram();

/* Append a mesasagebox to the obtained PeopleCode string */
&sProgram = &sProgram | "messagebox(0,"""",0,0, ""Modifying PeopleCode with PeopleCode!"");";

/* Update the PeopleCode for the Peoplecode Program */
Local any &test1, &test2, &test3;
Local boolean &bUpdatedPeopleCode = &oPeopleCodeProgram.UpdateProgram(&sProgram, &test1, &test2, &test3);

/* Throw an exception if the PeopleCode did not update */
If Not (&bUpdatedPeopleCode) Then
 throw CreateException(0, 0, "PeopleCode did not update");
End-If;

/* Update the PeopleCode Program Definition to save the change to the PeopleCode */
Local boolean &bDefnUpdated = &oPeopleCodeProgram.UpdateDefn();

/* Throw an exception if the definition did not update */
If Not (&bDefnUpdated) Then
 throw CreateException(0, 0, "Definition did not update");
End-If;

While I did not provide concrete examples of how to manipulate every possible object type with %metadata, I hope that I shined enough light on the subject to provide direction on how to go about manipulating any particular object type.  I believe that after gaining an understanding of the major aspects (Definitions, Managers, and Keys) of %metadata, one can fairly easily stumble their way through the usage of this package.

I think there can be many interesting use cases of the %metadata application package. I am personally putting this package to use by building out a PIA-based (online) PeopleSoft IDE. At the moment my online IDE is just a PeopleCode event editor with a horrible UI, but it is worth mentioning that I have had great success so far with using this package to view/update PeopleCode on any of the PepleCode events.  My biggest hurdle at the moment is coming with a JavaScript-based PeopleCode syntax highlighter/parser. I will be sure to document this project’s progress here.

Custom Fluid Homepage Background Image

$
0
0

I recently got my hands on a PeopleSoft environment running the new 8.56 PeopleTools.  I have been most curious to see the advancements in the Related Content Framework Event Mapping functionality in the new Tools release.  One huge limitation with Event Mapping in the 8.55 PeopleTools was the inability to inject custom styling into Fluid pages.  The framework did not disallow this practice, but injecting custom styles into Fluid pages would generally result in the page becoming incorrectly rendered and unusable.  One particular interesting use case of injecting custom styling with Event Mapping is to change the Fluid Homepage background.  This use case was proposed by Andy Dorfman of the psadmin.io Community and I had previously tried this in 8.55 and it did not work well. However, it seems to work great in the new 8.56 PeopleTools.  Below I will walk through how one can go about changing the Fluid homepage background with Event Mapping.

The first thing you are going to want to do is upload the desired background image into App Designer.  This step is optional, but doing this should allow for the image to load faster when the homepage is generated. The alternative is to store the image on an external server and then reference the remote image URL in the CSS in the next step.

App Designer Image

The next step is to create a custom style sheet in App Designer to set the homepage background as an image.  If you chose to upload the image into the PeopleSoft database, then you can make use of the %Image meta-html, otherwise you will have to reference the URL to the image file.  I am not the author of this CSS and I give credit and thanks to Jim Marion for sharing this snippet in the psadmin.io Community.

App Designer Style

Now you will need to write the Application Class PeopleCode to add the custom style sheet to the Fluid Homepage.  We can make use of the AddStyleSheet function to inject our custom style.

App Class PeopleCode

This code will need to be mapped to the preprocessing of the component pre build event of the Fluid Homepage CREF.  If you are unfamiliar with using Event Mapping to add code to the Fluid Homepage, then you can read my post Classic UI for Administrative Users.  This post contains the steps needed to map Application Class PeopleCode to the Fluid Homepage CREF.

After enabling the mapping, the custom style should get injected on the Fluid Homepage and your custom background image should display.

Here is one example:

Custom BG 1

And here is another:

Custom BG 2


Using Event Mapping to achieve a custom background image provides a lot of flexibility.  Since application code is used to serve the image, you have the potential to do conditional background images.  One example would be to display one image for one group of users and display a completely different image to another group of users.  You could even do something like show a particular image based on the time of day, the weather, or if it is the user’s birthday!


Adding Breadcrumbs to the Fluid Navigator

$
0
0

The Fluid Navigator is the new navigation technique that most closely resembles to old Classic style drop down menu navigation.  If you have not yet had the chance to convert your entire menu structure to use more modern navigation techniques, then you are stuck having to rely on the Fluid Navigator to get you where you need to be in some cases.  One major limitation of the Fluid Navigator is that is does not show you breadcrumbs when drilling down into the menu structure.  Not having breadcrumbs displayed makes it much harder to quickly jump around the menu.  In this post, I will demonstrate how you can add breadcrumbs to your Fluid Navigator.

This solution will be achieved using the Related Content Framework Event Mapping functionality.  We will be mapping two separate application classes to a couple of CREFs in the application.  One application class will be mapped to the component level post build event of the Fluid NavBar.

Fluid NavBar

And the other application class will be mapped to the component level post build event of the Fluid Navigator.

Fluid Navigator

I have included all of the applicable objects for this solution in an App Designer project.

CLICK HERE to download the project.

Project Overview:

Once you import this project into App Designer, you can open up the PSM_BREADCRUMB_NAV Application Package and take a peek at the two classes that will be getting mapped with Event Mapping.  The first class is named DisplayBreadcrumbs.  This class is used to keep track of the portal registry folder objects that the user drills into when navigating.  The folder names serve as the breadcrumbs and they will get displayed at the top of the Fluid Navigator as the user navigates.  When a user clicks on the breadcrumb, the Navigator content area will display the folder’s contents.

import PT_RCF:ServiceInterface;
import PTNUI:NavBarContentArea:NBContentCollection;
import PTNUI:NavBarContentArea:NBContent;

class DisplayBreadcrumbs implements PT_RCF:ServiceInterface
 method execute();
end-class;

/* This is a reference to the current nav bar content collection */
Component PTNUI:NavBarContentArea:NBContentCollection &ptnui_CurContentColl;

/* Use this function to get a reference to the appropriate portal */
Declare Function PortalOpen PeopleCode FUNCLIB_PORTAL.PORTAL_GEN_FUNC FieldFormula;

/* Use this function to set what shows up in the content (menu) area */
Declare Function SetContentArea PeopleCode PTNUI_DOCK_REC.PTNUI_NB_ACTION FieldFormula;

Declare Function GetLocalNodeContentURI PeopleCode PTNUI_NB_WRK.FUNCLIB FieldFormula;

method execute
 /+ Extends/implements PT_RCF:ServiceInterface.execute +/
 
 /* If the nav bar content collection is blank then return */
 If None(&ptnui_CurContentColl) Then
 Return;
 End-If;
 
 /* The FLDR parameter will be the folder that the user clicked on */
 Local string &sFolder = %Request.GetParameter("FLDR");
 
 Local string &standalone = %Request.GetParameter("sa");
 
 /* If no folder was passed in, then the user must be a the root */
 If None(&sFolder) Then
 &sFolder = "PORTAL_ROOT_OBJECT";
 End-If;
 
 /* Get a reference to the current portal */
 Local ApiObject &portal = PortalOpen();
 
 /* Get a reference to the current portal registry folder object */
 Local ApiObject &sParentFolder = &portal.FindFolderByName(&sFolder);
 
 Local integer &iBreadcrumbCount = 0;
 
 /* While the parent folder exists */
 While All(&sParentFolder)
 
 /* Generate the URL to invoke when the breadcrumb is clicked on */
 Local string &sUrl = GetLocalNodeContentURI() | "/c/NUI_FRAMEWORK.PTNUI_MENU_COMP.GBL?sa=" | &standalone | "&FLDR=" | &sParentFolder.name;
 If &standalone <> "y" Then
 &sUrl = "javascript:PTNavBar.OpenInContentArea('" | &sUrl | "&ICDoModal=1&ICGrouplet=1', '" | &sParentFolder.name | "');";
 End-If;
 
 /* Change the label for the Root folder */
 If (&sParentFolder.name = "PORTAL_ROOT_OBJECT") Then
 &sParentFolder.label = "Main Menu";
 End-If;
 
 /* Create NBContent object to represent the breadcrumb in the nav bar */
 Local PTNUI:NavBarContentArea:NBContent &oParentBreadCrumb = create PTNUI:NavBarContentArea:NBContent(&sParentFolder.name, &sParentFolder.label, &sUrl);
 
 /* Set a custom style to differentiate the breadcrumbs from the normal nav bar content items */
 If (&iBreadcrumbCount > 0) Then
 &oParentBreadCrumb.style = "psm_breadcrumb"; /* Parent breadcrumb */
 Else
 &oParentBreadCrumb.style = "psm_breadcrumb_selected"; /* Current breadcrumb */
 End-If;
 
 /* Set the breadcrumb to show at the top of the nav bar content collection */
 &ptnui_CurContentColl.InsertItemAtStart(&oParentBreadCrumb);
 
 /* Reference the current folder's parent */
 &sParentFolder = &portal.FindFolderByName(&sParentFolder.ParentName);
 
 &iBreadcrumbCount = &iBreadcrumbCount + 1;
 
 End-While;
 
 /* Set the content area equal to the modified nav bar content collection */
 SetContentArea(&portal, &ptnui_CurContentColl);
 
 /* Close the PortalRegistry object */
 &portal.close();
 
end-method;

The second class is named StyleBreadcrumbs.  This class is just used to inject custom styles and scripts when the NavBar is clicked by making use of the AddStyleSheet and AddJavaScript functions.  The main style sheet (PSM_BREADCRUMB) will be used to apply a special style to the breadcrumbs so that they cosmetically differ from non-breadcrumb items in the Fluid Navigator.

import PT_RCF:ServiceInterface;

class StyleBreadcrumbs implements PT_RCF:ServiceInterface
 method execute();
end-class;

method execute
 /+ Extends/implements PT_RCF:ServiceInterface.execute +/
 
 /* Hide the delivered Navigator header */
 AddStyleSheet(StyleSheet.PSM_HIDE_NAVIGATOR_HEADER);
 
 /* Override the size of the Navigator menu items */
 AddStyleSheet(StyleSheet.PSM_SMALL_NAVIGATOR_ITEMS);
 
 /* Apply a custom style to the breadcrumbs in the Navigator */
 AddStyleSheet(StyleSheet.PSM_BREADCRUMB);
 
 /* Automatically open the Navigator when the NavBar is clicked */
 AddJavaScript(HTML.PSM_OPEN_NAVIGATOR);
 
end-method;

The included PSM_BREADCRUMB style sheet in the project applies a rather basic styling to the breadcrumbs.  I am sure there is a way to make the breadcrumbs more visually appealing, but I do not have an eye for design.  I am certainly open to enhancement suggestions on this!

Project Configuration:

Now that we have a basic understanding of the technical details of this project, we are now ready to perform the Event Mapping configuration for this solution.  You will need to navigate to the Event Mapping tab of the Manage Related Content Service page (Main Menu > PeopleTools > Portal > Related Content Service > Manage Related Content Service) and select the “Map the event of the Application pages” link.  We are interested in performing mappings to the Navigator CREF and the NavBar CREF.

Content References

You are going to need to map the PSM_BREADCRUMB_DISPLAY Service to the post build/post processing of the Navigator CREF.

Navigator Event

You will need to map the PSM_BREADCRUMB_STYLE Service to the post build/post processing of the NavBar CREF.

NavBar Event

After successfully performing and saving the mappings, you should be all set.  All you need to do is log out and log back in to see the changes.

Here is an example of drilling down into the “Permissions & Roles” folder in the Fluid Navigator with this bolt on configured:

Permissions and Roles

In this example, clicking on the PeoleTools folder will bring up the PeopleTools folder contents in the Fluid Navigator content area like this:

PeopleTools

Additional Navigator Styling:

You probably noticed in the picture above that the menu items are smaller than they are as delivered.   This is because a custom style sheet (PSM_SMALL_NAVIGATOR_ITEMS in the project) was applied in the event mapped code to override the size of the menu items.  I find this style particularly helpful on large form factor devices.  The delivered large sizing of the menu items works well for phones and tablets since the user has less clicking precision.  However, for devices with a lot more screen real estate, I find the large menu items counterproductive. If you are not interested in applying this style to all form factors, then you could always evaluate the user’s form factor in the event mapped code to conditionally apply the custom style.

Another subtle difference is the picture above is the absence of the Navigator header that contains the buttons to go to previous directories.  I found the Navigator header to be useless with the breadcrumbs present.  This is because we can now simply click on the breadcrumb (folder name) to go back to previous directories.  The style sheet that makes this occur is included in the project as well and is named PSM_HIDE_NAVIGATOR_HEADER. Similar to the other custom style to adjust the menu item sizing, you have complete control in how you want to apply the style in the event mapped code.

If you decide to not apply the custom styles to alter the Navigator and are only interested in the breadcrumbs, then here is an example of what this would look like:

No Custom Nav Styling


While adding breadcrumbs to the Fluid Navigator will hopefully make using it easier, don’t use this as a reason to not explore other navigation techniques that may allow for an even better navigation experience.  There are many ways to perform navigation in a Fluid environment by implementing and using things like Homepages, Tiles, Navigation Collections, Search, etc.  I believe that the proper implementation of these more modern navigation techniques will provide for the best possible overall user experience.

Conditional Data Masking with Event Mapping

$
0
0

Using Event Mapping to perform field level data masking is an idea that I have toyed with since the release of Event Mapping in PeopleTools 8.55.  Event Mapping is a desirable tool for field level data masking in PeopleSoft because it can allow for bolt on runtime application logic to determine if a user should have the ability to view a particular piece of data.  I am unfortunately not here to say that Event Mapping can deliver us a (much needed) data masking framework for PeopleSoft applications.  However, I have recently found that Event Mapping is capable of satisfying one particular field level data masking requirement that I threw at it.  In this post, I will be sharing a proof of concept project that I’ve used to perform conditional data masking on a read only data field within a delivered Fluid page.

Project Overview and Requirements

My requirement is to mask the Social Security number that shows up on the Fluid Personal Details within Campus Solutions.

Personal Details Before

The field needs to get masked at component load time and it shall have the ability to be conditionally unmasked based on custom logic that evaluates the user’s session attributes.  The SSN field will get replaced with an image that provides an on click event.

Personal Details After

The outcome of the click event is determined at component load time and it provides for three possible outcomes: Click-To-Deny, Click-To-View, or Click-To-Challenge.  Below I will explain these three scenarios and how they are achieved.

Click-To-Deny:

This scenario is for when you do not want the user to have the ability to see the unmasked SSN.  When the user clicks the lock icon, a modal pops up that tells the user they are unauthorized to view the data.

Click To Deny

This scenario is achieved by discarding the SSN value in the component buffer at component load time and injecting an onload script that contains the logic to display the popup modal on the onclick event.

Click-To-View:

This scenario is used for when you want to allow for the user to view the data, but you still want the data to be masked at runtime.  This is desirable in circumstances where you want to prevent unnecessary sensitive data leakage.  If the user is performing a transaction on the page that requires the knowledge of the SSN, then the user can simply click to view the SSN.  Otherwise, the SSN will never be exposed to the client.

For the Click-To-View functionality to work, the original SSN value is encrypted at component load time and is communicated to the client’s browser via an injected onload script.  If the user needs to view the masked data, the encrypted value is sent to an IScript in the onclick event that will validate the request, log the transaction details, and respond back with the decrypted value.

Click-To-Challenge:

This is by far the most interesting scenario as it exercises a field level multifactor authentication solution for PeopleSoft.  In this scenario, if a user wants to view the masked data, then they will be prompted for a secondary form of authentication on the click event.

Click To Challenge

The Click-To-Challenge functionality is achieved via a combination of the methodologies used in the Click-To-Deny and Click-To-View functionalities.   One difference is that the popup modal contains an input form to allow receipt of the user’s secondary form of authentication.  The other difference is in the call to the IScript on the form submit.  The IScript behaves identical to the Click-To-View IScript except that it also validates the user’s secondary form of authentication before responding back with the decrypted value.

Project Implementation

If you are interested in implementing this proof of concept solution for the Campus Community Fluid Personal Details page in your Campus Solutions application, then follow the steps below.

CLICK HERE to download App Designer Project and import it into app designer. Map the included Related Content Service using Event Mapping to the Fluid Personal Details CREF.

Personal Details CREF

Map the PSM_MASK_PROFILE_SSN service to the Post Build/Post Process event

Map Event

Last, set the permission for the Click-To-View IScript and the Click-To-Challenge IScript.

Set Permissions

Note: You will need to implement a multi-factor authentication solution and extend the Click-To-Challenge IScript for it to serve its intended purpose.  I have documented how I’ve implemented Google Authenticator in PeopleSoft if you are interested in this form of secondary authentication.

It should be clear that this type of data masking solution will not work on every field in the application.  There are a couple of prerequisites that must be met in order for a solution like this to be considered.  This solution requires that the field to be masked must be read only and reside on a Fluid page.   Even then however, there still may be some nuances that will prevent this solution from working properly.  This demonstration was done on a Campus Solutions 9.2 application running PeopleTools 8.56.

Online PeopleCode Editor Project

$
0
0

I recently made a post discussing the %metadata application package in PeopleSoft.  I provided an overview of the package as well as examples of how to use it.  To support my research efforts in understanding this package, I made a simple web-based PeopleCode editor to edit PeopelCode events in the PIA.  My initial plan was to try build out a web IDE for accessing and modifying PeopleTools objects.  I have found myself busy working on other projects and wanted to at least share what I was able to create.  This project is simply an IScript that serves a basic interface to view and modify PeopleCode events using the %metadata application package.  In this post, I will walk through how to install and use this online PeopleCode editor project.  Please note that this project is a proof of concept and it is not intended to be used in any production PeopleSoft environments.

CLICK HERE to download the Online PeopleCode Editor project.  Unzip the file and copy the project from file into App Designer.

The only setup that is needed to get this project working is the assignment of a Permission List. Login to the PIA and assign the PSM_WIDE Permission List to a role that you want to have access to the PeopleCode editor.

Assign Permission List

Using the user account with the appropriate role, navigate to Main Menu > PSM Projects > Web IDE.  Select the object type of PeopleCode program that you want to open.

Select Object Type

Populate the key values for the object type, select the name of the event, and then click the Open button.

Select Event Name

If you provided key values to a valid PeopleCode program, then you should get presented with a garbled mess of an entire PeopleCode event represented as a raw string.  In this example, I have opened the PeopleCode on the Post Build event of User Profiles component.  At this point you can modify the code and click the Save button to save the changes.  If there are any syntax errors, then the IScript should respond with an error message.  However, the IScript is not coded to have robust error handling.

In this example, I added a simple message box to the end of the code and clicked the Save button.

Edit PeopleCode

Now you can navigate to the component to invoke the modified PeopleCode event to see the changes.

Test Changes

While this tool is really raw and cosmetically unappealing, I believe it does a decent job of demonstrating how the %metadata application package can be used to programmatically modify PeopleCode events.  I think this tool can be enhanced on many different fronts and would be interested in hearing if anyone is able to make this tool more usable.

Where in the Fluid Am I?

$
0
0

When navigating in a PeopleSoft system that uses Fluid Navigation, it can be easy to lose your bearings in terms of where you actually are in the Portal Registry.  Knowing the exact Content Reference (and how to get to it) in Structure and Content is sometimes crucial when troubleshooting issues.  The problem is that the new Fluid Navigation does not directly correlate to the structure of the Portal Registry like it used to in Classic (breadcrumb) Navigation.  This results in there being no easy way to determine where a given Fluid page is in Structure and Content.  I have recently found that using the combination of a simple Bookmarklet and IScript to be sufficient enough to reveal the Portal Registry path for the pages that I navigate to.  In this post, I will share the implementation details of this helpful utility.

CLICK HERE to download the app designer project.  Unzip the project from the downloaded file and import the project from file in App Designer.

There is a Permission List in the project named PSM_YAH.  This Permission List has access to the IScript that provides the Portal Registry path for a given page.  Assign the PSM_YAH Permission List to a Role that you want to have this functionality enabled for.

Assign Permission List

Now you should be able to invoke the IScript without receiving an authorization error.  You can call the IScript by pasting in the following value into your web browser after authenticating into the application.

<domain>/psc/ps/<portal>/<node>/s/WEBLIB_PSM_YAH.ISCRIPT1.FieldFormula.IScript_YouAreHere

You should receive a response that states “Current Page Information Not Provided”.

To test the IScript with actual values, you can provide the menu and component URL query parameters to fetch the Portal Registry path for the values.  Below is an example call for the User Profiles Portal Registry path.

<domain>/psc/ps/<portal>/<node>/s/WEBLIB_PSM_YAH.ISCRIPT1.FieldFormula.IScript_YouAreHere?menu=MAINTAIN_SECURITY&component=USERMAINT

The script’s response in this case should be “Root > PeopleTools > Security > User Profiles > User Profiles”.

Alternatively, you can provide the url to a given Content Reference as a query parameter and the script will respond with the Portal Registry path to the Content Reference. Here is an example to get the Portal Registry path for the “User Profiles” Content Reference via its URL:

<domain>/psc/ps/<portal>/<node>/s/WEBLIB_PSM_YAH.ISCRIPT1.FieldFormula.IScript_YouAreHere?url=http://<domain>/psc/ps/<portal>/<node>/c/MAINTAIN_SECURITY.USERMAINT.GBL

The functionality provided by the IScript is helpful, but the call to the IScript needs to be made more functional.  To achieve this, I took some pointers from Jim Marion’s blog posts on bookmarklets and PeopleSoft JavaScript development techniques, and created a “You Are Here” bookmarklet.  I think exposing the Portal Registry path information for a PeopleSoft page through a bookmarklet provides an acceptable level of usability.  To make use of the bookmarklet, drag the link below into your browser’s bookmark bar.

PS You Are Here

Now when you are on a PeopleSoft page and you have access to the IScript mentioned above, you can click the bookmarklet to get a div element injected into the page that contains the Portal Registry path information for the current page.  Here is an example of when I invoke the script on the Fluid Addresses page:

Fluid Addresses

And here is the script’s output for the Fluid Homepage:

Fluid Home

As you can see, each breadcrumb in the outputted Portal Registry path is clickable.  Clicking the breadcrumb will take you to the corresponding Content Reference in Structure and Content.  When I click the Fluid Homepage breadcrumb, I am taken to the Structure and Content for the Fluid Homepage Content Reference.

Fluid Home CREF

The script is also capable of giving the Portal Registry path for non-Menu/Component based Content References.  For example, my online PeopleCode editor is an IScript based Content Reference.  When I invoke the bookmarkelt on this particular page, the script responds with the correct Portal Registry path.

IScript CREF

I will admit that there are some rough edges with this utility, but I have found it to be very useful for the most part. While this tool is helpful in determining how to get to a particular Content Reference in Structure and Content, it fails to provide the actual path that a user took to get to the given page. For example: Which homepage the user came from, which tile the user clicked on, etc. Dan Iverson has an idea in the My Oracle Support Community PeopleTools idea space that seems to propose this functionality. I think having this sort of tracking functionality baked into the application could be useful in troubleshooting and replicating issues.

Code References

Bookmarklet JavaScript:

(function() {
 var xhttp = new XMLHttpRequest();
 
 xhttp.onreadystatechange = function() {
 if (this.readyState == 4 && this.status == 200) {
 var yah = (doc.getElementById('youarehere') ? doc.getElementById('youarehere') : doc.createElement('div'));
 yah.id = 'youarehere';
 yah.style = 'text-align: center; border:solid black 1px;';
 yah.innerHTML = this.responseText;
 var bodyEl = doc.getElementsByTagName('BODY')[0];
 bodyEl.insertBefore(yah, bodyEl.firstChild);
 }
 };

 var currUrl = (!!frames['TargetContent'] ? !!frames['TargetContent'].strCurrUrl ? frames['TargetContent'].strCurrUrl : window.location.href : window.location.href);
 var parts = currUrl.match(/ps[pc]\/(.+?)(?:_(\d+))*?\/(.+?)\/(.+?)\/[chs]\//);
 var doc = (frames['TargetContent'] ? frames['TargetContent'].document : document);
 var divId = (doc.getElementById('pt_pageinfo') ? 'pt_pageinfo' : parts[2] ? 'pt_pageinfo_win' + parts[2] : 'pt_pageinfo_win0');
 var pageInfo = doc.getElementById(divId);
 var menu = (pageInfo ? pageInfo.getAttribute('menu') : '');
 var component = (pageInfo ? pageInfo.getAttribute('component') : '');
 var mode = (pageInfo ? pageInfo.getAttribute('mode') : '');
 var portalNeeded = (frames['TargetContent'] ? 'n' : 'y');
 var scriptUrl = window.location.origin + '/psc/' + parts[1] + '/' + parts[3] + '/' + parts[4] + '/s/WEBLIB_PSM_YAH.ISCRIPT1.FieldFormula.IScript_YouAreHere?url=' + encodeURIComponent(currUrl) + '&menu=' + encodeURIComponent(menu) + '&component=' + encodeURIComponent(component) + '&p=' + portalNeeded;

 xhttp.open('GET', scriptUrl, true);
 xhttp.send();
}())

IScript PeopleCode:

Declare Function PortalOpen PeopleCode FUNCLIB_PORTAL.PORTAL_GEN_FUNC FieldFormula;

Function IScript_YouAreHere()
 Local string &sUrlParam = Unencode(%Request.GetParameter("url"));
 Local string &sMenu = Unencode(%Request.GetParameter("menu"));
 Local string &sComponent = Unencode(%Request.GetParameter("component"));
 Local string &sPortalNeeded = %Request.GetParameter("p");
 
 /* If the required parameters are not provided, then output a message */
 If (None(&sMenu) Or
 None(&sComponent)) And
 None(&sUrlParam) Then
 %Response.Write("Current Page Information Not Provided");
 Return;
 End-If;
 
 Local ApiObject &portal = PortalOpen();
 Local ApiObject &sCurrCref;
 
 /* First, try to find the CREF by using the provided Menu and Component */
 If &portal.FindCRefByURL(GenerateComponentContentURL(%Portal, %Node, @("Menuname." | &sMenu), "GBL", @("Component." | &sComponent), "", "")) <> Null Then
 &sCurrCref = &portal.FindCRefByURL(GenerateComponentContentURL(%Portal, %Node, @("Menuname." | &sMenu), "GBL", @("Component." | &sComponent), "", ""));
 Else
 /* Second, try to find the CREF by using the provided url (including url query parameters) */
 If (&portal.FindCRefByURL(&sUrlParam) <> Null) Then
 &sCurrCref = &portal.FindCRefByURL(&sUrlParam);
 Else
 /* Third, try to find the CREF by using the provided url (Excluding url query parameters) */
 If (&portal.FindCRefByURL(Split(&sUrlParam, "?")[1]) <> Null) Then
 &sCurrCref = &portal.FindCRefByURL(Split(&sUrlParam, "?")[1]);
 Else
 /* If all three attempts of getting the current CREF fail, then output a message */
 %Response.Write("No Content Reference Exists for the Provided Page Information (URL = " | &sUrlParam | ", " | "Menu = " | &sMenu | ", " | "Component = " | &sComponent | ")");
 Return;
 End-If;
 End-If;
 End-If;
 
 /* Check if portal wrapper is needed */
 Local string &sSCCrefLinkBase;
 Local string &sSCFldrLinkBase;
 If &sPortalNeeded = "y" Then
 &sSCCrefLinkBase = GenerateComponentPortalURL(%Portal, %Node, MenuName.PORTAL_ADMIN, "GBL", Component.PORTAL_CREF_ADM, "", "");
 &sSCFldrLinkBase = GenerateComponentPortalURL(%Portal, %Node, MenuName.PORTAL_ADMIN, "GBL", Component.PORTAL_OBJ_LIST, "", "");
 Else
 &sSCCrefLinkBase = GenerateComponentContentURL(%Portal, %Node, MenuName.PORTAL_ADMIN, "GBL", Component.PORTAL_CREF_ADM, "", "");
 &sSCFldrLinkBase = GenerateComponentContentURL(%Portal, %Node, MenuName.PORTAL_ADMIN, "GBL", Component.PORTAL_OBJ_LIST, "", "");
 End-If;
 
 /* Get the current CREF's parent folder */
 Local ApiObject &sParentFolder = &portal.FindFolderByName(&sCurrCref.ParentName);
 
 /* Get the link to the CREF in Structure and Content */
 Local string &sSCLink = &sSCCrefLinkBase | "?PORTALPARAM_PNAME=" | &sParentFolder.Name | "&PORTALPARAM_CNAME=" | &sCurrCref.Name;
 Local string &sYouAreHere = "<a href=" | &sSCLink | ">" | &sCurrCref.Label | "</a>";
 
 While (&sParentFolder <> Null)
 /* Get a link to the parent folder in Structure and Content */
 &sSCLink = &sSCFldrLinkBase | "?PORTAL_NAME=" | %Portal | "&PORTALPARAM_FNAME=" | &sParentFolder.Name;
 &sYouAreHere = "<a href=" | &sSCLink | ">" | &sParentFolder.Label | "</a>" | " > " | &sYouAreHere;
 /* Get the parent folder */
 &sParentFolder = &portal.FindFolderByName(&sParentFolder.ParentName);
 End-While;
 
 %Response.Write(&sYouAreHere);
 
 &portal.close();
 
End-Function;

Generating QR Codes in PeopleSoft

$
0
0

The PeopleCode language is not known for natively supporting cutting edge technical functionalities.  However, it is common for the PeopleSoft Developer to be thrown a technically advanced requirement from time to time.  When this sort of occasion arises, I like to extend PeopleCode with Java.  The possibilities are practically endless when extending PeopleCode with Java.  The problem though, is that the PeopleSoft app server may not be equipped with the proper Java packages to execute the required Java functions.  While we have the ability to deploy additional Java classes to the PeopleSoft app server, this practice is not always acceptable.  A clever alternative is to use the built-in JavaScript interpreter in Java and write JavaScript to overcome the technical hurdle.   In this post, I will demonstrate how I am able to use Java’s ScriptEngineManager class to execute JavaScript to generate QR codes in PeopleSoft.

QR codes are a great way to transport information into a mobile device that would otherwise be tedious to input manually.  QR codes have been widely adopted in the marketing space, but there are many other great use cases for QR codes.  One use case for QR codes is for transporting secret keys into mobile authenticator applications such as Google Authenticator or Authy.  Once the key information has been communicated, the mobile application can begin generating Time-Based One-Time Passwords (TOTPs) for the user.

For demonstration purposes, I created a simple IScript that is capable of returning a QR code for the logged in user.  The data inside of the QR code is a 16 character, base32 encoded string.  This string is used as the secret key for a mobile authenticator application to generate TOTPs for the user.

CLICK HERE to download the app designer project.  Unzip the project from the downloaded file and import the project from file in App Designer.  To access the QR code generating IScript, you will need to assign the PSM_QR Permission List to a Role of the users that you want to generate QR codes for.

After performing the security setup, you can login as the privileged user and invoke the IScript.  You can point your browser to the following URL to generate a QR code for the user:

<domain>/psc/ps/<portal>/<node>/s/WEBLIB_PSM_QR.ISCRIPT1.FieldFormula.IScript_GenQR

And you should get a QR code for the logged in user:

QR Code

This QR code can be scanned into a mobile authenticator application and it should immediately start generating TOTPs.

QR code generation is a neat functionality, but what I really want to highlight on is how simple the application PeopleCode is behind the IScript.  I start off by generating the URL that I want to create the QR code based off of.

/* Generate random 16 character Base32 string */
Local array of string &sBase32Chars = CreateArray("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "2", "3", "4", "5", "6", "7");
Local integer &i;
Local string &sKey;
For &i = 1 To 16
  &sKey = &sKey | &sBase32Chars [Int(Rand() * 32) + 1];
End-For;

/* Supply the (arbitrary) domain name for the account to be associated with */
Local string &sHost = "peoplesoftmods.com";

/* Generate the URL to be scanned into the authentication app */
Local string &sQRUrl = "otpauth://totp/" | %UserId | "@" | &sHost | "?secret=" | &sKey;

Next, I feed the generated URL into some JavaScript code that is capable of generating QR codes in SVG format.  This JavaScript code gets executed by Java’s built-in JavaScript interpreter. After the interpreter executes the script, I am able to reference the SVG image output “qrSvg” variable using the built-in get method.

/* Get the QR Code SVG JS library and the JS command to generate a QR Code from a given value */
Local string &sJSprogram = GetHTMLText(HTML.PSM_QRCODE_SVG) | GetHTMLText(HTML.PSM_QR_CODE, &sQRUrl);

/* Use the Java ScriptEngineManager to run the JavaScript program to create the QR Code */
Local JavaObject &manager = CreateJavaObject("javax.script.ScriptEngineManager");
Local JavaObject &engine = &manager.getEngineByName("JavaScript");
&engine.eval(&sJSprogram);

/* Get the outputted SVG image from the JavaScript variable */
Local string &sSVGImage = &engine.get("qrSvg").toString();

Last, I output the SVG QR code and additional details to the screen using the write method of the %Response class.

/* Output the SVG image and the account details */
%Response.Write("<br><b>Scan the QR code or enter the secret key into your authentication app</b><br>");
%Response.Write(&sSVGImage);
%Response.Write("<br>Account Name: " | %UserId | "@" | &sHost);
%Response.Write("<br>Secret Key: " | &sKey);

In a previous post, I demonstrated how I was able to use the Google Charts API to generate QR codes in PeopleSoft.  Using the third-party API was an easy solution to the problem, but I think it is best to limit relying on third-parties as much as possible. In this post, we saw how we can utilize “delivered” techniques to generate QR Codes without creating additional dependencies.

I think using Java’s Built-in JavaScript interpreter to perform technically challenging tasks is a really cool way to both overcome the shortcomings of the PeopleCode language and provide for a lifecycle management-friendly solution.  If you are interested in reading and understanding more about this technique, then I highly suggest checking out Jim Marion’s posts JavaScript on the App Server: Scripting PeopleCode and Dynamic Java in PeopleCode.

Compile and Run PeopleCode Online

$
0
0

There are many times where I come across some sample PeopleCode on the internet and I want to execute the PeopleCode in my environment to see the output.  The route I take to test drive some PeopleCode usually involves me opening up an existing object in App Designer, pasting in the sample code, and then going to the PIA to see the results.  I find this process rather tedious to perform just to see the output of some sample code.  Not to mention, I have to make sure I go back into App Designer and clean up the object I modified.  More often than not, I already have a PeopleSoft session open in my web browser when I am exploring PeopleCode online.  So what I decided to do was build an online utility for compiling and running PeopleCode directly in the PIA.  In this post, I will share this helpful utility for anyone that is interested in this functionality.

This solution follows a recurring theme of using the %metadata Application Package to update PeopleTools-managed objects through PeopleCode.  I have a couple of recent posts on this topic, one is on understanding the %metadata Application Package and the other is about using the %metadata Application Package to create an online PeopleCode event editor.  The technicalities of this solution are very similar to that of the online PeopleCode editor project.

CLICK HERE to download the App Designer project for this utility.  Unzip the file and copy the project from file into App Designer.

The only setup that is needed to use this utility is the assignment of a Permission List. Login to the PIA and assign the PSM_RUN_PC Permission List to a role that you want to have access to the PeopleCode executer.  The user that gets access to the PeopleCode executer must also have the necessary privileges to update Application Package objects.  This is because the utility updates an Application Class method with the inputted PeopleCode to run.

After performing the security setup, you can login as the privileged user.  The utility can be found by navigating to Main Menu > PSM Projects > Run PeopleCode.

Run PeopleCode

On this page you will be greeted with an empty input box, a save button, and a run button.  The usage of the utility is simple:  Paste in some PeopleCode, click the save button to compile the PeopleCode, and then click the run button to execute the PeopleCode.

In this example I wanted to run some sample PeopleCode to output the PS_HOME and PS_CFG_HOME environment variables.

Local string &sOutput;
&sOutput = "PS_HOME: " | GetEnv("PS_HOME") | Char(10);
&sOutput = &sOutput | "PS_CFG_HOME: " | GetEnv("PS_CFG_HOME");
MessageBox(0, "", 0, 0, &sOutput);

After pasting in the code and clicking the save button, the PeopleCode is ready to be executed. In this example clicking the run button results in a messagebox displaying the environment variables.

Output

How it Works

The PeopleCode behind the save button is responsible for updating the Application Class PeopleCode of the generic RunPC Application Class within the PSM_RUN_PC Application Package.  The code uses the %metadata Application Package to compile and save the Application Class PeopleCode.  Here is the code behind the save button that updates the Application Class PeopleCode using %metadata:

import %metadata:Key;
import %metadata:PeopleCodeProgram:*;
import %metadata:AppPackageDefn:*;

Local %metadata:PeopleCodeProgram:PeopleCodeProgram_Manager &mgrPeopleCodeProgram = create %metadata:PeopleCodeProgram:PeopleCodeProgram_Manager();
Local number &int1, &int2;
Local string &strErr;
Local boolean &bSaved, &bResult;
Local %metadata:PeopleCodeProgram:PeopleCodeProgram &defn;

/* Create the key to reference the generic RunPC Application Class */
Local %metadata:Key &key = create %metadata:Key();
&key.AddItem(key:Class_ApplicationPackage, "PSM_RUN_PC");
&key.AddItem(key:Class_ApplicationClass, "RunPC");
&key.AddItem(key:Class_Method, "OnExecute");

/* Display an error if the object definition doesn't exist */
If Not &mgrPeopleCodeProgram.DefnExists(&key) Then
 throw CreateException(0, 0, "Definition Error");
End-If;

/* Get an updateable object definition so that we can overwrite the existing PeopleCode */
&defn = &mgrPeopleCodeProgram.GetDefnToUpdate(&key);

/* Insert the new PeopleCode into the Run method within the boilerplate Application Class code */
&appClassText = "class RunPC method Run();end-class; method Run " | PSM_RUN_PC_WK.PCTEXT.Value | " end-method;";

/* Attempt to compile the Application Class PeopleCode */
&bResult = &defn.UpdateProgram(&appClassText, &strErr, &int1, &int2);

/* Display an error if the code failed to compile */
If Not (&bResult) Then
 /* I think &int1 and &int2 can be used to determine where exactly in the code the error occured */
 throw CreateException(0, 0, MsgGetText(158, 20153, "PeopleCode Error") | " " | &strErr);
End-If;

/* Attempt to update the object definition */
&bSaved = &defn.UpdateDefn();

/* Display an error if the object definition failed to update */
If Not (&bSaved) Then
 throw CreateException(158, 20152, "Error saving new PeopleCode.");
End-If;

/* Enable the Run button and disable the Save button */
PSM_RUN_PC_WK.BUTTON1.Enabled = True;
PSM_RUN_PC_WK.BUTTON.Enabled = False;

If the provided PeopleCode is invalid, then an error would be displayed to the user when they click the save button.  If the code compiles and saves successfully, then the following code behind the run button is used to execute the updated Application Class method that contains the PeopleCode to run:

import PSM_RUN_PC:RunPC;

Local PSM_RUN_PC:RunPC &oRunPC = create PSM_RUN_PC:RunPC();

/* Execute the updated Run method */
try
 &oRunPC.Run();
catch Exception &e
 /* Enable the Save button and disable the Run button */
 PSM_RUN_PC_WK.BUTTON.Enabled = True;
 PSM_RUN_PC_WK.BUTTON1.Enabled = False;
 throw CreateException(0, 0, &e.ToString());
end-try;

/* Enable the Save button and disable the Run button */
PSM_RUN_PC_WK.BUTTON.Enabled = True;
PSM_RUN_PC_WK.BUTTON1.Enabled = False;

While this is a helpful utility, it should be well understood that enabling this type of functionality for users in the PIA could result is some serious security implications.  I would also like to point out that the delivered Application Class Tester utility within Enterprise Components could potentially be leveraged to perform similar functionality as the utility presented in this post.  If you are interested in playing with that utility, then it can be found under Main Menu > Enterprise Components > Component Configurations > Application Class Tester.

Response Manipulation with Portal Custodian

$
0
0

What in the world is the Portal Custodian?  I asked myself this very question when I came across a delivered file named portalCustion.xml on the PeopleSoft web server.  The Portal Custodian is an undocumented functionality that allows for regular expression pattern matching replacements on the portal content served by the web server.  The portal content is the “wrapper” that the psp servlet puts around the page content.  This means that we have the ability to modify the contents within the portal header and footer before the client receives the response from the web server.  I discovered and tested this functionality in a PeopleSoft application running PeopleTools 8.56, but it is quite likely that this feature exists in older Tools releases.   In this post, I will walk through how we can use this interesting feature to manipulate response data.

The Portal Custodian functionality operates off of a configuration file on the web server named portalCustodian.xml.  This file can be found in the following directory:

%PIA_HOME%\webserv\<domain>\applications\peoplesoft\PORTAL.war\WEB-INF\psftdocs\ps

The XML file contains a list of actions for the Portal Custodian.  Each action represents a replacement that the Portal Custodian should perform.  Each action contains four parameters:

  • name – A hardcoded value set to “ReplaceAll”
  • contenturlpattern – A regular expression pattern that, if found in the URL, will prompt the Portal Custodian to attempt string replacements in the response
  • pattern – A regular expression pattern that, if found in the response, will be replaced with the specified replacewith string value
  • replacewith – A string value that will replace all of the matched patterns

The behavior of the Portal Custodian seems to indicate that the code makes use of Java’s replaceAll method to perform the replacements on the responses.  So this means that all occurrences of the pattern will get replaced with the replaceWith string.

Here is an example action that will perform a script injection on each response served by the psp servlet.  The script being injected will log a “Hello” message to the console on each page load.

<action>
  <name>ReplaceAll</name>
  <contenturlpattern>.*</contenturlpattern>
  <pattern>&lt;\/html&gt;</pattern>
  <replacewith>&lt;script&gt;console.log(&quot;Hello&quot;);&lt;/script&gt;$0</replacewith>
</action>

In the above example, the greater than and less than signs within the pattern and replacewith tags needed to be encoded for the action to work.  After adding an action, a web server restart is required.


Easy REST Requests

$
0
0

One of the biggest pain points with using Integration Broker to consume third-party REST web services is the creation of all of the required metadata definitions.  If I want to perform a simple REST request to a third-party URL, then I am stuck having to create Message, Service, Service Operation, and Routing definitions.  Sometimes I just want the ability to test an API without having to create all of these definitions.  It turns out that there are a couple of delivered methods within the %IntBroker class that allow developers to code the consumption of a REST API without the need to create all of the metadata definitions listed above.  The two methods that I will be discussing are the ProvideRestTest and ConnectorRequest methods.

The ProvideRestTest Method

Delivered usage of the ProvideRestTest method can be found behind the Integration Broker Service Operation Tester Utility.  This utility is located under Main Menu > PeopleTools > Integration Broker > Service Utilities > Service Operation Tester.  It appears that this method was created to ease the process for this utility to make generic REST requests.

Although undocumented, the ProvideRestTest method is included in the %IntBroker class and can be used within a PeopleCode program.  Below is a sample of code that makes use of this method to send a simple POST request to a third-party URL:

/* Get the request message from a generic REST-based Consumer Service Operation */
Local Message &mRequest = CreateMessage(Operation.IB_GENERIC_REST_POST);

/* Set the third-party URL to send the request to */
&mRequest.OverrideURIResource("http://httpbin.org/post");

/* Populate the request message with data */
Local boolean &bRet = &mRequest.SetContentString("My Data");
&bRet = &mRequest.AddSegmentHeader("MY-CUSTOM-HEADER", "XXX");

/* Perform HTTP POST */
Local Message &mResponse = %IntBroker.ProvideRestTest(&mRequest, "POST");

/* Output the reponse code and response content */
MessageBox(0, "", 0, 0, String(&mResponse.HTTPResponseCode));
MessageBox(0, "", 0, 0, &mResponse.GetContentString());

 

The ConnectorRequest Method

The ConnectorRequest method of the %IntBroker class is the more well-known (and documented) method that allows to make easy REST requests to third-party URLs without having to do a ton of setup.  Documentation and sample usage of this method can be found in the Bypassing Integration Engines to Send Messages section of PeopleBooks as well as in the snippet below:

ConnectorRequest

The biggest caveat that I have found to using the ConnectorRequest Method for REST requests is that the resulting response message does not contain the populated HTTPResponseCode property.  This forces the developer to decipher the status of the response by parsing the body of the response message. The fact that the HTTP status code is absent from responses, makes the ConnectorRequest method not very friendly for working with REST responses.  Other than this shortcoming, I believe the ConnectorRequest method is great for making easy requests.

Important details

One important piece of information to note is requests that get sent with the ProvideRestTest and ConnectorRequest methods will not appear in the Synchronous Services Operation Monitor.  I believe this is due to these methods bypassing the Integration Engine when sending the request messages.  This appears to be true regardless of the specified value for the Log Detail field of the corresponding Service Operation Routing.

Another important detail that often goes overlooked when consuming secure (https) third-party REST APIs is that the third-party SSL certificates must be added to the PeopleSoft keystore in order to send request messages to the third-party URL.

Closing Thoughts

Both of these methods are suitable for making easy REST requests in PeopleCode.  As we saw in this post however, the ProvideRestTest method is undocumented (unsupported) and the ConnectorRequest method has a vital shortcoming that makes it less appealing to be used for making REST requests.  Hopefully we will see delivered support for the ProvideRestTest method and/or additions to the ConnectorRequest method in future PeopleTools releases.

PeopleCode Syntax Highlighting in Ace Editor

$
0
0

Ace is an embeddable code editor written in JavaScript.  My first exposure to the Ace Editor was when I started to use the Cloud9 IDE for non-PeopleSoft development.  I like using web-based tools because they prevent me from being tied to a particular machine to do work.  With tools like Cloud9, I can develop software from any one of my internet-connected devices.  PeopleSoft development is a bit different than developing software in other languages as App Designer is needed to edit PeopleCode programs. I would rather not have to always rely on a client-based application to edit PeopleCode.  This is the reason that I embarked on writing a JavaScript-based PeopleCode editor powered by Ace.  The Ace Editor provides many desirable features that can be found in most modern editors and it also allows for language-specific syntax highlighting. Today I would like to share the PeopleCode syntax mode that I created for the Ace Editor.

Usage of the PeopleCode syntax highlighter in the Ace Editor is really simple.  Below is an example that demonstrates the functionality.

The syntax highlighter is not perfect, but it is definitely a step up from no highlighting at all.  I hope to provide updates to the highlighting rules as I have time to the GitHub repository here.  I certainly welcome any community contributed enhancements to this repository as well.

While PeopleCode syntax highlighting with the Ace Editor is helpful for displaying read-only PeopleCode snippets online, this functionality really becomes useful when used in an online PeopleCode editor that is capable of making real time code updates to the application.  I did a post last year discussion the idea of exposing the %metadata Application Package as an API of sorts to support the backend of an online PeopleCode editor.  While this was a solid proof of concept, the demonstrated editor in that post was undoubtedly hideous.  Since then I have added a slew of changes with the most important one being the incorporation of the Ace Editor.  Here is a screen shot of the progress that has been made to the online PeopleCode editor project:

Online PeopleCode Editor

As you can probably tell, the Ace Editor with PeopleCode syntax highlighting makes the online PeopleCode editor much more usable.   I am excited to share and document the new features and functionality of the online PeopleCode editor project in a future post.

Overriding Web Server Properties in the PIA

$
0
0

A while back I did a tutorial on how to define your own web profile custom properties.  In that post I demonstrated how custom meta-HTML variables can be defined on the web profile that can then be used in the server’s static HTML files.  This technique is good for providing an easy way to manage the web server properties in the PIA.  Something that I did not mention in that post is that we can use the web profile custom properties page in the PIA to manage (and override) existing web server properties defined on the server.  I have had good experiences using this technique to override delivered web server properties defined in the various .properties files on the web server.  An example use case of this technique is to override properties in the text.properties file to achieve JavaScript injection on the PeopleSoft sign in page.  In this post, I will demonstrate how we can use this technique so that we can have a custom sign in experience without having to customize delivered files.

The text.properties file holds key-value pairs containing messages that get displayed on the various HTML files on the web server.  The variable of interest for performing JavaScript injection on the sign in page is #146.

TextProperties

This property is used for copyright text that gets displayed at the bottom of the signin.html file.  The reason I like to use this variable for JavaScript injection on the sign in page is because of the way that it is embedded in the signin.html file.

SignInHTML

Since the <%=146%> meta-tag is directly in between other HTML tags, I can override this variable with a custom script that will perform the customizations to the sign in page.  This allows me to easily (ab)use property #146 as a built-in hook that will allow for a tailored sign in page that is customization-free.

Managing custom property #146 via the text.properties file is undesirable as I want the ability to change the injected script without having to have access to the web server file system.  Fortunately, we are able to use the Custom Properties tab on the Web Profile Configuration page in the PIA to override the value for property #146.  We do not need to worry about the value defined in the text.properties file as the value specified on the web profile will take precedence.  You will simply need to set the Property Name to 146, Validation Type to String and Property Value to the custom script to modify the signin.html page.  As you can see from the picture below, you can prepend the script to the desired footer text so that you do not lose out on the message from getting displayed at the bottom of the sign in page.

WebProfCustProp

After making this change and restarting the web server, you will see that your custom value in property #146 is injected in the footer of the sign in page.  Here is some sample JavaScript that can be used to target and modify various elements on the signin.html file (Note: Tested in 8.56.03).

Here is how the sign in page looks from storing the above fragment in the web profile custom property:

CustomSignIn

The injected script performed various alterations to the sign in page.  The script changed field labels, added an additional input field, and set the footer text with a dynamic year.  With the ability to inject custom JavaScript into the sign in page, the possible sign in page transformations are practically endless.

Managing Large HTML Objects

$
0
0

I wanted to share a quick tip on managing third-party JavaScript libraries in HTML objects in PeopleTools.  The tip is to manage these HTML objects in the Branding Framework in the PIA rather than using App Designer.  I have found that App Designer does not play well with handling large JavaScript libraries as it tends to add newlines in the code after line lengths reach a certain point.  Minified JavaScript libraries are notorious for having extremely long line lengths and this is an issue for App Designer.

For example, let’s say you wanted to store and serve the minified jQuery v3.3.1 JavaScript library from a custom HTML object.  You would copy and paste the JavaScript code into a new HTML object definition in App Designer and save the new definition.

Save App Designer

At this point you could locally serve the jQuery library by using %Response.GetJavaScriptURL(). This is when you will be greeted with an error similar to the following:

Unexpected Token

The error “Uncaught SyntaxError: Invalid or unexpected token” is a result from a line of code in the JavaScript unexpectedly ending.  This is due to the way that App Designer saved the HTML object.

If you want to successfully serve large JavaScript libraries from PeopleTools HTML objects, then you will need to use the Branding Framework in the PIA (Main Menu > PeopleTools > Portal > Branding > Branding Objects) to create the new HTML object to house the JavaScript code.

Save Branding Framework

I have found that the Branding Framework does not add newlines for long line lengths like App Designer does.

It is also very important to note that you should use the Branding Framework to edit these large HTML objects and not just for creating them.  If you edit a large HTML object (created with the Branding Framework) in App Designer, the object will get corrupted when you save it.  This will result in the “Unexpected Token” error and you will have to recreate the HTML object in the Branding Framework.

This does not happen with every large JavaScript library, but rather only the ones that have long line lengths.  However, I have been bit by this App Designer shortcoming so many times that I have gotten into the habit of always using the Branding Framework to manage large HTML objects to avoid potential headaches with using App Designer.

ACM Plugin Configuration Properties

$
0
0

I am finally starting to get up to speed with Automated Configuration Management (ACM) Plugins.  ACM is something that the guys over at psadmin.io have been talking about for some time now and I think this is a great new PeopleTools functionality.  I have experienced an unfortanute limitation around the allowed character length of the input configuration properties for the some of the ACM plugins that I am currently working on. It turns out that the input configuration properties for ACM plugins are limited to a measly 254 characters.  This is a problem for plugins that require lengthy configuration properties.  For the plugins that I am creating, I wanted a way to easily create and mange plugin configuration properties without having any character length constraints.

My hack around this limitation is to use HTML objects created in the Branding Framework to house my lengthy input configuration values for the plugins.  This allows me to modify and maintain the configuration values in the PIA (Branding Objects page) as well as not be limited in the character length of the data.

Config Data

Then on the page to set the plugin configuration properties, I simply reference the HTML object name that houses the lengthy configuration data.

Config Property

Since we cannot use the GetHtmlText function in an Application Engine program, I had to do another hack to read in the content from the HTML object in the plugin’s configureEnvironment method.  Fortunately, there is a delivered API that allows us to read in the HTML object content data as a string.  I find this API easier to use versus having to query the underlying tables in the DB.

This is how I get the config data from the HTML object in the plugin’s configureEnvironment method:

/* Get the HTML object name */
Local string &sHtmlObjName = &variables.get("env.certobj");

/* Instantiate HTMLDefn object and attempt to open it */
Local PT_METADATA:MetaDataAPI:HTMLDefn &oHtml = create PT_METADATA:MetaDataAPI:HTMLDefn(&sHtmlObjName);
Local boolean &bRet = &oHtml.OpenReadOnly();

/* Error if the object doesn't exist */
If Not (&bRet) Then
   throw CreateException(0, 0, "%1 HTML Object does not exist", &sHtmlObjName);
End-If;

/* Get the config data defined in the HTML object */
Local string &sConfigData = &oHtml.ContentData;

This technique could potentially work with storing and fetching lengthy configuration properties for any ACM Plugin.  While this method is not as clean as creating a record, page, component, etc. to manage lengthy configuration variables, I think this technique can be a viable option in some cases.

Viewing all 45 articles
Browse latest View live