Protect your APEX app from URL Tampering – in just a few clicks

Recently I’ve been reviewing and updating my knowledge of APEX security, especially protection from URL tampering. I’ve read the documentation, a number of blogs, and heard from people with experience in the field such as Lino. By default, when you create a new application in APEX you get the following security settings set automatically, which is a good start:

  • Application Session State Protection is Enabled.
  • Each page has Page Access Protection set to Arguments Must Have Checksum.
  • Each Application Item has Protection Level set to Restricted – May not be set from browser.
  • Each Primary Key Item* created by a wizard has Protection Level set to Checksum Required – Session Level.

(* that is, any item mapped from a table column that is, or forms part of, a Primary Key constraint).

These default settings are considered best practice. If you change these, it becomes your responsibility to ensure that your application is protected against security vulnerabilities from URL tampering.

For page items, however, the Protection Level defaults to Unrestricted. This is ok for Form items because the page fetch process will set their values on page load, rendering any attempt at URL tampering ineffective.

For non-form page items, unless the Page Access Protection is relaxed (Unrestricted), leaving items unrestricted is safe since URL tampering is blocked for the entire page anyway. At runtime, if a malicious visitor tries to modify the item value via the URL, they will get the error “No checksum was provided to show processing for a page that requires a checksum when one or more request, clear cache, or argument values are passed as parameters.

Error message "No checksum was provided to show processing for a page that requires a checksum when one or more request, clear cache, or argument values are passed as parameters."

However, what if a developer later needs to change the page to Unrestricted? They may unwittingly introduce a potential URL tampering issue because one or more items were not protected.

UPDATE: in fact, this applies even if it’s a different page in the same application. As Martin D’Souza pointed out a decade ago, URL tampering for any item in the application is possible from any page in the application that is Unrestricted.

The majority of these items are editable input items, so the fact that someone may input a value via the URL is not a big deal. However, for Hidden and Display Only items, it is common for application logic to depend on their values; this logic may be adversely affected by malicious values supplied via the URL.

In some cases, this default is needed in order for the application to work. Some examples when an item must be left Unrestricted are:

  • An item is changed by a Dynamic Action (whether via a Set Item Value, via the Items to Return of a Execute Server-side Code action, or in some custom JavaScript), and cannot have Value Protected set because the page may be submitted.
  • We do actually intend the item to be set via the URL, e.g. when an external web page has a link that sets the item’s value.

In all these cases, the application must be designed to ensure it does not “trust” the value of these items; it should apply suitable checks to ensure the values are valid.

In most cases, it is best practice to set the item Protection Level to Checksum Required – Session Level (or Restricted – May not be set from browser where supported).

You can use a query like this to discover all items that may need to be reviewed:

select
    i.application_id,
    i.page_id,
    i.page_name,
    i.region,
    i.item_name,
    i.display_as
from apex_application_page_items i
where i.application_id = :app_id
and i.item_protection_level = 'Unrestricted'
and i.display_as_code in ('NATIVE_HIDDEN','NATIVE_DISPLAY_ONLY')
order by i.application_id, i.page_id, i.region, i.item_name;
Report showing APPLICATION_ID, PAGE_ID, PAGE_NAME, REGION, ITEM_NAME, DISPLAY_AS, listing two Display Only items (P1_DISPLAY_ONLY_ITEM, P1_DISPLAY_ONLY_ITEM_DA) and two Hiden items (P1_HIDDEN_ITEM, P1_HIDDEN_ITEM_DA) that have Item Protection Level set to "Unrestricted".

Other excellent options are using third-party security scanners such as APEXSec and APEX-SERT to alert you to potential issues in your application. We mandate the use of tools like these internally at Oracle for our own applications and they are a great help.

Using the Session State Protection Wizard

One easy step you can take right now is to use the Session State Protection wizard. It gives you a quick overview of what level of protection your application has against URL tampering, and gives an easy way of fixing the relevant attributes in bulk.

You can access the wizard via Shared Components > Session State Protection

Screenshot of part of App Builder Shared Components; under the Security heading, we want to click on "Session State Protection".

Alternatively, you can access the wizard via Edit Application Definition > Security > Session State Protection > Manage Session State Protection

Screenshot of part of the App Builder Edit Security Attributes page, under the Security tab. In the Session State Protection section, we want to click on the button "Manage Session State Protection".

The wizard starts by showing an overview of the current state of your application’s protection against URL tampering.

Screenshot of the App Builder Session State Protection overview page.
It indicates that my application has Session State Protection = Enabled.
It shows that one page is set to "Arguments Must Have Checksum", one page allows "No URL Access", and one page is "Unrestricted".
It indicates that all 9 Page Items in the application are set to "Unrestricted".
It indicates that of the application's Application Items, one is set to "Restricted - May not be set from browser", one is "Checksum Required - Session Level", and one is "Unrestricted".
Next to each category a ">" icon button is shown.
At the bottom of the page is the button "Set Protection".

You can see if your application has Session State Protection enabled (which it should, really), and if any pages, page items, and/or application items are unprotected. In my sample app here, it’s obvious that there are some potential security issues that need to be reviewed.

You can click the > buttons next to each category to list all the pages and items that need to be reviewed.

The main things to watch out for are Pages, Page Items, and Application Items that are set to Unrestricted. Other values are generally fine.

If you see any Items which are set to Checksum Required but not at the Session Level, you may find that a developer has simply set them incorrectly and you should consider changing them to Session Level. However, there are some scenarios where the other levels (Application Level, or User Level) are required.

Now, I might now go through the application page-by-page and set the protection level on each page and item as appropriate. This could be a laborious process for a large application.

A good alternative is to use this wizard to set the protection level in bulk. In this case, I’m going to click Set Protection.

Screenshot of page 1 of the Session State Protection Wizard.
It is asking to Select an Action - either "Disable", or "Configure". I have selected "Configure".
Buttons at the bottom of the page allow me to Cancel, or go to the Next page.
I’ve selected the action Configure, then click Next.
Screenshot of page 2 of the Session State Protection Wizard.
This page allows me to select the Page Access Protection (defaulted to "Arguments Must Have Checksum"), the Page Data Entry Item Protection, the Page Display-Only Item Protection, and the Application Item Protection. These last three are all defaulted to "Checksum Required - Session Level".
Buttons at the bottom of the page allow me to go to the Previous page, Cancel, or go to the Next page.
The wizard now gives me the opportunity to modify the protection level on my pages and items in bulk. I’m going to accept the defaults (Arguments Must Have Checksum / Checksum Required – Session Level) because they are appropriate for most cases in my application.
Screenshot of the last page of the Session State Protection Wizard.
This allows me to confirm the changes that will be made to pages and items in the application.
Buttons at the bottom of the page allow me to go to the Previous page, Cancel, or Finish.
After reviewing the summaries of the changes that the wizard will make, I click Finish.
Screenshot of the App Builder Session State Protection overview page, after running the wizard.
The page now indicates that all 3 pages in my application are set to "Arguments Must Have Checksum", that all 9 Page Items and the 3 Application Items are now set to "Checksum Required - Session Level".

Perfect!

Final Steps

Now, I need to check for hidden page items that are now restricted that might need to be returned to Unrestricted. Otherwise, users will see the error “Session state protection violation” when they submit the page, if a dynamic action has changed them.

Screenshot of an error message alert, saying "1 error has occurred: Session state protection violation: This may be caused by manual alteration of protected page item P1_DISPLAY_ONLY_ITEM_DA. If you are unsure what caused this error, please contact the application administrator for assistance."

The following query will alert me to any Hidden items that have Value Protected switched off (e.g. because they need to be submitted):

select
    i.application_id,
    i.page_id,
    i.page_name,
    i.region,
    i.item_name,
    i.display_as
from apex_application_page_items i
where i.application_id = :app_id
and i.item_protection_level != 'Unrestricted'
and i.display_as_code = 'NATIVE_HIDDEN'
and i.attribute_01 = 'N' -- Value Protected
order by i.application_id, i.page_id, i.region, i.item_name;
Report showing APPLICATION_ID, PAGE_ID, PAGE_NAME, REGION, ITEM_NAME, DISPLAY_AS listing one entry for "P1_HIDDEN_ITEM_DA", a hidden item that is set to "Unrestricted" and has attribute_01 (Value Protected) set to "N".

Now I can review this item to check if Value Protected really needed to be switched off. If the page is never submitted, or the item is never changed by any dynamic actions, this could be switched On. Otherwise, I need to set the item protection to Unrestricted in order for the page to work.

Having made changes to the application, I need to test to ensure I haven’t introduced any issues. My focus will be mainly on the following areas:

  1. Navigation – e.g. do the View or Edit buttons in all reports still work?
  2. Dynamic actions – e.g. do all the dynamic actions and custom javascript still work on all pages that set item values?

For #1, I’m looking for any links that include item values that were not correctly built. If the application generates any links using just string concatenation, it will fail if the target page expects a checksum. The application should build these links using declarative link attributes if possible, or by calling apex_page.get_url (or apex_util.prepare_url at least).

For #2, I would test to ensure that after triggering a dynamic action or javascript code that modifies an item’s value, that the form is still submitted (saved) without error.

Further Reading

Thanks to Christian Neumueller for his review and comments on this article.


Reusable Region as a Modal Page

"clone" icon

On a number of pages throughout my application, I needed to build a region containing a fairly complex set of items, along with dynamic actions and other controls to provide a friendly editing experience for the user. This non-trivial set of items with their accompanying dynamic actions and conditions would be needed on several different pages, and in some cases, multiple times on the same page.

Copying all this all over the place would have created a maintenance headache, so I would much prefer to build them only once, and then re-use the same component throughout my application. Unfortunately, APEX does not at this stage support the concept of a reusable region. An idea might be to allow a region to “subscribe” to another region – although this would be tricky because somehow the item names, dynamic action names, etc. would need to be unique but predictable.

Why not use a plugin?

One approach is to build the whole region as a plugin; this would be ideal as the plugin can then be maintained separately and deployed wherever it’s needed; this would have the benefit that it could be reused in multiple applications.

The downside is that I would not be able to use the declarative features of APEX to define the items and dynamic actions within the region; I would have to code most of that in custom HTML, JavaScript and AJAX calls for database interaction. This would then provide a different maintenance challenge for my successors.

Why not put the region on the Global Page?

Another approach would be to build the region on the Global Page; a condition could be used to show it if it’s needed by the current page.

The downsides to this approach include: (a) you can’t reuse it multiple times on a single page; (b) it may be tricky to integrate it on the pages it needs to return data to (although this could be done with some JavaScript); and (c) you have little control over where on each page the region would be shown.

The Global Region idea might work better if is implemented as an Inline Dialog; with some JavaScript it could be made to pop up wherever it’s needed. I haven’t tried this approach, however.

Use a Modal Page

Instead, the approach I took was to use a modal page. This is a page that will pop up as a layer on top of the calling page, making the calling page visible but non-responsive until the user closes the popup. I can then define all the items needed, along with their conditions and dynamic actions, in the one modal page, and then add buttons throughout my application wherever it was needed.

The calling page needs to pass the current value of one or more items to the modal page; these values are not in the database (yet) because the user may be in the middle of editing them, so their current value on screen may be different to the value stored in the table. This means I can’t have the modal page reading the value from the table, and I can’t just pass the value using the link attributes because these are set in stone when the page is rendered.

In order to open the modal page, then, I need to use a dynamic action.

Note that you can’t build the URL for the modal page in JavaScript, because the client-side code cannot calculate the checksum required by the modal page. Instead, I pre-calculate the URL for the modal page using apex_page.get_url which generates the checksum automatically.

When the user clicks the “Edit” button, it needs to first copy the current value of the item into the session state for the modal page; I do this by making the Edit button Defined by Dynamic Action. On click, it executes two actions: (1) Server-side Code to submit the current value of the text item and set the modal item’s value; then (2) JavaScript Code to redirect to the URL I calculated earlier.

The modal page is then shown, allowing the user to make changes to the value. When they click the “OK” button, the modal page closes and returns the value via Items to Return.

Note that the modal page itself never saves any changes to the database, since on the calling page, the user might decide to cancel.

Back on the calling page, the new value is copied back into the page item via a Dialog Closed dynamic action. This sets the value based on the Dialog Return Item.

I’ve built a “dummy” sample app to demonstrate this technique. You can try it out, and download the sample app definition, from here: https://apex.oracle.com/pls/apex/jk64/r/demo-reusable-modal/home

Details

Here is my main page definition, with two regions. Each region has an item that we want to pass to/from our modal page.

Page designer showing two regions defined, "Region 1" and "Region 2"

Each region needs a unique Static ID.

Page designer showing the Static ID for "Region 1" is set to "region1"
Page designer showing the Static ID for "Region 2" is set to "region2"

Each region has a visible Value item, an Edit button, and a hidden item to precalculate the URL for the modal page.

Page designer showing Region 1 has an item "P1_VALUE1", a button "EDIT1", and a hidden item "P1_EDIT_URL1". Region 2 similarly has an item "P1_VALUE2", a button "EDIT2", and a hidden item "P1_EDIT_URL2".

There are no special attributes on the value item(s); they could be a simple text field, a text area, a readonly item, a combination of various item types, or they could be hidden. Typically they would be based on database column(s) and saved in the record being edited.

The “EDIT URL” hidden items are precalculated using an expression, and set to Always, replacing any existing value in session state.

Page designer showing the PL/SQL Expression as the source for P1_EDIT_URL1.

The other edit URL is similar.

Page designer showing the PL/SQL Expression as the source for P1_EDIT_URL2.

The call to apex_page.get_url is used to pass some static values (that are not changed by the page at runtime) to the modal page. These values may be used by the modal page to customise it for the context it was called from.

apex_page.get_url(
    p_page => 'modal',
    p_items => 'P2_ID,P2_OPTION',
    p_values => :P1_ID || ',' || 'Region 1',
    p_triggering_element => '$(''#region1'')'
)

Note that the value of the item is not passed in the URL.

Note that p_triggering_element is a string, constructed to be a jQuery selector referring to the Static ID that was set on the region, so that the right Dialog Closed event will fire (since we may have multiple Edit buttons on the same page).

Tip: if your modal page doesn’t need them, you can omit the p_items and p_values parameters.

The Edit buttons are set to “Defined by Dynamic Action“.

Page designer showing EDIT1 with a dynamic action "on click edit1".

The Server-side Code simply copies the current value of the item into the modal page’s item. This sets the session state on the server, which is then loaded when the modal is opened.

Page designer showing the Execute Server-side Code action runs the code ":P2_VALUE := :P1_VALUE1;", with Items to Submit set to "P1_VALUE1".

The JavaScript Code redirects to the modal page using the URL we calculated on page load.

Page designer showing the Execute JavaScript Code action.
apex.navigation.redirect("&P1_EDIT_URL1.");

The JavaScript Code for Region 2 is the same except it refers to P1_EDIT_URL2.

On page 2, the modal page, I have contrived an example “calculator” which simply breaks the string value into two “parts”, and allows the user to edit each “part” separately; when they click OK, the concatenated value gets returned to the calling page.

Page designer for page 2 ("modal"). It has two editable items "P2_PART1", "P2_PART2", as well as a "hidden items" region with "P2_VALUE", "P2_ID" and "P2_OPTION". The page also has a button region with "CANCEL" and "OK" buttons.

The two “PART” items are calculated on page load with some PL/SQL:

The page has an After Header process "init" which computes some value for P2_PART1 and P2_PART2 based on the value of P2_VALUE. (The expression itself is not important.)

Note that this code is being executed based on the value of P2_VALUE which was set in session state by the calling page.

Just for the sake of the demo, my “calculator” merely sets the value of the hidden P2_VALUE item based on concatenating the two “parts”:

Dynamic action on change of P2_PART1 and P2_PART1 executes some JavaScript which sets the value of P2_VALUE based on the entered values of P2_PART1 and P2_PART2.

Note: you would define whatever items, dynamic actions or other components that you need.

This modal page never saves any changes to the database; that’s the role of the calling page.

The OK button simply closes the dialog, returning the new value of P2_VALUE to the calling page.

Page designer showing the "on Click OK" dynamic action. It runs the Close Dialog action. The Items to Return is set to P2_VALUE.

Back on the calling page, each region has a dynamic action defined on Dialog Closed.

Page designer on page 1, within Region 1, a dynamic action "on close modal1" on the event Dialog Closed.

The Set Value action copies the Dialog Return Item value into the appropriate item on the page.

Page designer on page 1, the "on close modal1" dynamic action runs a Set Value to set the item P1_VALUE1 to the Dialog Return Item, P2_VALUE.

Summary

To use my special modal page in my application, I need to:

  1. Set a unique Static ID on the region
  2. Add an Edit button with a dynamic action
  3. Add a hidden URL item based on an expression
  4. Add a dynamic action to the region on Dialog Closed

The outcome is that the modal page provides a user-friendly experience involving any complex items, lists, dynamic actions, conditions, etc. maintained in one place, which can be re-used anywhere needed in the application.

If you would like to examine in detail the demo app, you can download it from here: https://apex.oracle.com/pls/apex/jk64/r/demo-reusable-modal/home (click the “Download this demo app” link). You may then install this in your own workspace and check out how it all works.

Have you had a similar requirement in your apps? Comment below and describe how you implemented it.


Friendly URL structure

Oracle has updated apex.oracle.com to APEX 20.1 which includes among other features the new “Friendly URL” option. The legacy URL structure concatenated a string of parameters into a single “p” parameter, which works fine; but it can make it difficult to configure web server rules to match and rewrite URLs. Apart from the application ID or alias (the first part), all the parameters are optional; if all were specified the URL will be something like this (line breaks added for clarity):

f?p=SAMPLE_DB_APP:HOME:16801234560918:myrequest:mydebug
  :myclearcache:myitems:myvalues:myprinterfriendly
  &p_trace=mytrace
  &cs=19A56DBFDXXXE4DF293C96D786
  &c=jk64

In this example, the application alias is used (SAMPLE_DB_APP) and page alias (HOME) followed by the session ID etc. I’ve also specified the workspace (jk64) using the “c” query parameter.

In APEX 20.1 if you edit your Application Properties, you will see the new “Friendly URLs” setting.

With this setting turned on, URLs generated throughout the application will take this form (this is not exhaustive, there are other query parameters supported as well):

/pls/apex/jk64/r/sample_db_app/home
   ?myitems=myvalues
   &request=myrequest
   &clear=myclearcache
   &debug=mydebug
   &printerFriendly=myprinterfriendly
   &session=16801234560918
   &p_trace=mytrace
   &cs=19A56DBFDXXXE4DF293C96D786

When the documentation says the URLs should be “easier to read” I’m pretty sure they mean “by developers”, not end users. I suppose this means the URLs make more sense to developers of other web-based applications, because they conform better to REST URL conventions.

In its simplest form, the URL generated by APEX will include just the workspace path prefix, application, page, and session ID:

 /pls/apex/jk64/r/sample_db_app/home?session=16801234560918 

Calling APEX_PAGE.GET_URL generates the URLs correctly for the application according to whether the Friendly URL setting is on or not.

I noticed the following features of this new format:

  • The URL generated by APEX_PAGE.GET_URL now includes a full path (excluding the domain).
  • Instead of the “c” workspace query parameter, the application’s Path Prefix (which could be set to something other than the workspace name) is used (jk64 in my example). This is not the workspace name, although in most cases it usually will happen to be the same because it’s defaulted that way. This attribute is set at the workspace level, under Administration / Manage Service / Set Workspace Preferences / SQL Workshop.
  • If not specified, the application and page alias will be used rather than the application or page ID, which is nice.
  • Even if you specify the application or page alias in uppercase, APEX_PAGE.GET_URL returns them in all lowercase.
  • The more important attributes relevant to a user navigating the application are now further towards the start of the URL, such as page and item values, so they will be more likely to be noticed by the end user.
  • The “/r/” bit in the URL is just that. It’s just “r” and can’t be anything else, don’t ask me what it means. EDIT: apparently it stands for “router”

If your users have bookmarked your application using the legacy URL format, you can still safely upgrade your application to use Friendly URLs because both are still supported. This also means that if you have some old code that generates links programmatically they should still work the same (although it is best practice to call APEX_PAGE.GET_URL for this purpose).

In case you’re wondering, it is not possible to change the URL format when calling APEX_PAGE.GET_URL, it will follow your application’s Friendly URL setting. If you call APEX_PAGE.GET_URL for another application, it will return the correct format of URL for the target application. If you call APEX_PAGE.GET_URL for an application that does not exist, it will return the URL in the legacy format.

Existing applications after upgrading, or ones you import from an older version of APEX, will still use the legacy URL syntax. New applications will use the new Friendly URLs by default – but you can revert them to the legacy URLs if you wish.

On a side note, in earlier versions when you create a new application the application alias was set to the application ID by default. In APEX 20.1, a new application will have an application alias generated from the initial application name; when I tried it, it added a number as well for some reason.

For obvious reasons, existing code that parses the URL (e.g. in javascript on the client) will probably break. This is a fairly rare thing but does happen (such as in a plugin of mine which I’ll need to fix).

At this very early stage, the legacy URL format is still fully supported – I imagine it will eventually be deprecated, but not yet.

Finally, I’d like to point out some entries on the APEX Feature Requests site:

Our patience has been rewarded 🙂


“Before Header” Branch

It’s well known that after processing a page one often needs to add a Branch so the user is taken to another page, e.g. to start the next step in a process. It’s less common to need a Branch that is evaluated before the page is shown. This is a “Before Header” Branch and I use it when the user might open a page but need to be redirected to a different one.

A good example is a page that is designed to direct the user to two or more different pages depending on what data they requested.

Another way that a branch like this can be useful is where a user might navigate to a page that should show a record, but if the record ID is not set, I might want the page to automatically redirect the user back to a report page instead.

Of course, you have to take some care with these sorts of branches; if the target page also has its own “Before Header” branch, that will also be evaluated; if the user ends up in a loop of branches the page will fail to load (with a “ERR_TOO_MANY_REDIRECTS” error).

This is the order in which various steps are done before APEX shows a page:

  1. Authentication check
  2. Verify session
  3. “Before Header” Branches
  4. Authorization check
  5. Computations
  6. “Before Header” Processes
  7. Regions, etc.

One of the implications of the above order is that any computations or processes that set or change application state (e.g. an application item) are not run before it evaluates conditions or attributes for any “Before Header” Branches. This applies regardless of whether the computations or processes are defined on the page, or defined globally in Shared Components. This little detail tripped me up today, because I had a branch that I needed to run based on a condition that relied on state that should have been set by a “Before Header” process defined globally.

If you need to redirect a user to a different page on the basis of any application state that is set by a computation or process, you can use a PL/SQL Process instead that does the redirect. The PL/SQL code to use is something like:

apex_util.redirect_url(p_url => apex_page.get_url(p_page => 1));

Warning: redirect_url raises the exception ORA-20876: Stop APEX Engine, so it’s probably preferable to avoid combining this with other PL/SQL code that might need to be committed first.


Add a “Share Link” to your application

Sometimes my customers need to be able to get a direct link to a record in their APEX application which they can share with others (e.g. to publish it on a website, or send by email to a colleague).

They don’t want people to be able to easily “guess” the link (so it needs to have some degree of randomness and complexity), but it’s not so sensitive that if someone somehow gets the link by mistake it won’t be cause for alarm. They would like to be able to invalidate the link at any time, so if they send the link and then decide they don’t want people viewing the record anymore, they can mark it as “expired”.

Task 1. Generate the link

We have a table to which we want to create links for each record. It already has a surrogate key called id based on a simple sequence, so on its own this is not suitable for our link (we don’t want users just adding 1 to the id and iterating through all values to look up other records). Add a column to the table with a unique constraint:

alter table mytable add (
  link_code varchar2(50)
, constraint mytable_link_code_uk unique (link_code)
);

Update existing rows (if any) and make the column not null:

update mytable
set link_code = id || '-' || dbms_random.string('x',10)
where link_code is null;

alter mytable modify link_code not null;

Set its value using the following expression (e.g. this could be done in a page process, in your table API, or a table trigger):

create trigger bi_mytable before insert on mytable for each row
begin
  :new.link_code := :new.id || '-' || dbms_random.string('x',10);
end;

Note that the random string is more than likely to be unique for each record; but we prepend the unique ID as well which guarantees it to be unique. In addition, if a user sends us a link saying “this didn’t work”, we can visually parse it to see what the record ID was they were trying to access. This is needed because our design allows users to overwrite link_code (e.g. to expire the link).

Task 2. Build an APEX link page

Create a page (in my example, this will be page 100) to be the link resolver. It won’t have any complex regions or show any information, but will merely redirect the user to the page with the actual data (page 10, in my example).

Set it up as follows:

  • Alias: GO
  • Page Access Protection: Unrestricted
  • Region with title “Record not found”
  • Hidden item: P100_REF
    • Session State Protection: Unrestricted

Note: if the target page that shows the record is Public (i.e. the people getting the link cannot login to the application), this link resolver page will also need to be public – i.e. set Authentication to Page Is Public. On the other hand, if the target record is only available to authenticated users, the link resolver page should be restricted to authenticated users as well.

Add a PL/SQL Process that runs Before Header, with the following code:

declare
  l_id number;
begin

  select x.id into l_id
  from mytable x
  where x.link_code = :P100_REF;

  -- requires APEX 5.1 or later
  apex_util.redirect_url(
    apex_page.get_url
      (p_page        => 10
      ,p_clear_cache => 10
      ,p_items       => 'P10_ID'
      ,p_values      => l_id));

/*
  -- use this on APEX 5.0 or earlier
  apex_util.redirect_url(
    apex_util.prepare_url(
      p_url => 'f?p='
            || :APP_ID
            || ':10:'
            || :APP_SESSION
            || ':::10:P10_ID:'
            || l_id));
*/

exception
  when no_data_found then
    null;
end;

If the page gets a valid value for P100_REF, it will find the ID for the corresponding record and redirect the user to the page showing that record. If the link is invalid or expired, the page will not redirect but will show a “record not found” message.

Task 3. Show the link

We show the generated link on a page with an ordinary Text item.

Create an item on the page with the following properties:

  • Name: P10_LINK
  • Custom Attributes (or HTML Form Element Attributes): readonly
  • Session State Protection: Unrestricted

We need to generate the link using the page’s domain name, which (at least in some cases) we don’t know ahead of time. To do this, we need to get the page’s URL including host, port and path.

Create a dynamic action on page load. Set its condition so it only fires if P10_LINK_CODE is not null (if it doesn’t already exist, create P10_LINK_CODE as a hidden item based on the database column LINK_CODE).

The dynamic action needs two True Actions – firstly, Execute Javascript:

$s("P10_LINK", window.location.protocol + '//'
               + window.location.hostname
               + ':' + window.location.port
               + window.location.pathname);

Secondly, Execute PL/SQL:

:P10_LINK := :P10_LINK
          || '?p=&APP_ALIAS.:GO:::::P100_REF:'
          || :P10_LINK_CODE;

This dynamic action constructs the link using the current window’s URL including path (which includes the trailing “/apex/f” or “/ords/f“) and query string (“?p=...“).

When the user clicks the item, we want it to automatically select the entire link and copy it to their clipboard. To show this has happened, we show the word “Copied” on the screen.

  1. Create a Dynamic Action on the item – Event: Click
  2. Add a True Action on the Dynamic Action
    1. Action: Execute JavaScript Code
    2. Fire On Page Load: (unchecked)
    3. Code:
this.triggeringElement.select();
document.execCommand("copy");
$(this.triggeringElement).after(" Copied.")

Users who are looking at the record get a “Share Link” in a convenient item that they can copy-and-paste into emails or web pages. If you need to expire a link, simple update the record with a new link_code and the old links will no longer work.


APEX Reports: One Link, Multiple Destinations

Every Interactive Report has an optional set of “Link” attributes that allow you to specify the location where the user should be redirected if they click a link next to a record in the report. You can choose “Link to Custom Target” and use the Link Builder to easily specify the target application, page, item values to pass, and other attributes.

linkbuilder1

What if the report combines different entities, and you need to direct the user to a different page depending on the type of entity? Or, if you need to direct the user to a different page with different attributes depending on the status of the record?

One method is to generate the URL in the report query using apex_page.get_url (APEX 5+) or apex_util.prepare_url (APEX 4 or earlier), or (God forbid) you could generate the url yourself using string concatenation.

A more declarative solution is to instead use APEX page redirects. This solution involves the following:

  1. Add some hidden items to the page to store the parameters for each target page;
  2. Add a Branch to the page for each target page; and
  3. Add a Request to the link to signal the page that a redirect has been requested.

NOTE
This method does not work as described if you want to navigate to a modal page.

Here’s an example. My page 550 has an interactive report which combines TI records with TRQ records (both of which have a very similar structure). If the user clicks on a TI record they should be redirected to p510 with the corresponding TI_ID, and if they click on a TRQ record they should be redirected to p305 with the corresponding TRQ_ID.

Here’s the link attributes for this report:

linkbuilder2

Notice that the page now redirects back to itself with the request set to “GOTO_TARGET”, along with the IDs required. My report query has been constructed so that every record will only have a TI_ID or a TRQ_ID, never both at the same time; so the link will ensure that only one of the P550_GOTO_xxx_ID values will be set.

The page then just needs two Branches: one for each target. Conditions on each branch ensures they only get activated if the request has been set, and the branch is selected based on which “GOTO ID” item has been set:

branch1
branch2

For a normal navigation to this report (e.g. from another page), the request should be blank (or some other value) so none of the Branches should be triggered.

For a relatively simple scenario like this, I like the declarative approach. Each branch can take advantage of the full range of APEX features such as the link builder, security conditions, build options, etc.

Note: this method works just as well for Classic report links as well.

The thing to be mindful of is that the order of the branches, and the condition on each branch, must be carefully selected so that the correct branch is activated in each situation. I’ve shown a simple example which works because I have ensured that only one of the ID parameters is set at the same time. If a record has both IDs, the condition on the first branch “GOTO_TARGET (TI_ID)” will evaluate to True and it will be used, regardless of what GOTO_TRQ_ID was set to.

If there were numerous potential destination pages, with a large number of parameters to pass, I might choose the apex_page.get_url method instead. This is also what I’d usually do if the target was a modal page; alternatively, I’d use a “redirector” modal page as described in the comments below.

Related