David Hockenbroch · Feb 13, 2024 go to post

I've never done it this way, just by overriding those methods in the class definition containing the property, but I do know in that case Set has to take a value as an argument. Maybe it's not recognizing your Set method because it doesn't match the signature of the usual Set method. When you override it in a class, it looks like this:

Method NewProperty1Set(Arg As %String) As %Status [ ServerOnly = 1 ]

In that case, Arg is the value that the process is trying to set the value of the property to. So it might have to be something like:

Method Set(Arg as %String){ S %val = "asd" Q 1}

Or if you wanted to some something with the input value, you could use Arg to do that?

David Hockenbroch · Feb 7, 2024 go to post

I don't have an answer, but I also hope someone does. // is specifically for a single-line comment, so it's odd to me that it's continued like that.

David Hockenbroch · Jan 25, 2024 go to post

To create a task in the task manager, you have to write a class that extends %SYS.Task.Definition. Within that class, you create an OnTask method, and that the method that will run when your task is triggered. There's a more thorough example of this here.

David Hockenbroch · Jan 18, 2024 go to post

Yes, you do have to tread lightly and program very carefully if you mess with those. Errors in them could make it impossible to start or log into IRIS. This is straight from the documentation about those routines:

Make sure that the routines are well-behaved under all possible conditions. They should be written defensively. That is, they should check to make sure that all the resources needed to complete their task are at hand and, if possible, reserved to them before computation starts. Errors which occur are reported as failures of that system function so it is important to think about the design from the viewpoint of error containment and handling. Failure to properly account for recovery in the face of missing resources or the presence of errors has varied consequences: InterSystems IRIS may fail to start; major functions such as Studio may act strangely; or more subtle and insidious consequences may occur which are not immediately detected. It is strongly recommended that these routines be carefully written and debugged under simulated conditions, and then tested under simulated environment conditions before being put into production systems.

It doesn't sound like you're trying to do anything too crazy, but do make sure you trap or catch any potential errors anyway.

David Hockenbroch · Jan 11, 2024 go to post

Emil, if the request is not coming from a form with the enctype set to multipart/form-data, can you give us an example of how the request is being created? Maybe the issue is that the request has not been created properly.

David Hockenbroch · Jan 11, 2024 go to post

When using $EXTRACT, you use a * to signify an offset from the end of a string. So if you did $EXTRACT(Str,1,*-1) you would have the string with the last character removed.

Also note that the arguments for the $EXTRACT are the string, the starting character, and the ending character, so in the examples you gave, you're actually telling it to extract from Str starting at Length(Str)-1. You need to have a 1 in there as the second argument to go from the beginning to that character.

David Hockenbroch · Jan 2, 2024 go to post

You say you've been able make the tables read only and not "everything". What are the accessing besides tables?

David Hockenbroch · Dec 29, 2023 go to post

You can also use the $ROLES special variable to do that. It contains both the user's assigned roles and any roles you've added during the process. You can't change the user roles, but if you set it that will add a role. So you could do:

set $ROLES = "%All"

Or whatever role you need to add, then do the stuff that requires that roles, then do:

set $ROLES = ""

That will take away the %All role you added, reverting the process to just the user's normal roles.

David Hockenbroch · Dec 20, 2023 go to post

If your IDList is a string, you'd have to use WHERE 10 %INLIST $LISTFROMSTRING(IDList,' ') so that the string is converted to an actual list for %INLIST to work.

David Hockenbroch · Dec 19, 2023 go to post

Maybe that's old code that's been carried over through some copying and pasting. I have a Cache 2012 instance and Name is not required in it.

David Hockenbroch · Dec 15, 2023 go to post

In my opinion, if you have a page that doesn't necessarily need to have any user interface elements - %CSP.StreamServer is a good example - that's the primary case for extending %CSP.Page. If you need a UI, then a csp page is easier to deal with.

David Hockenbroch · Nov 9, 2023 go to post

My mistake, Scott, I thought you just had Apache running on its default ports and were trying to figure out how to make the other requests also go there.

David Hockenbroch · Nov 9, 2023 go to post

The requests using port 52773 aren't even going to get to Apache, so you can't solve this problem by changing your Apache configuration. You would have to make your router forward traffic between your server and your other PCs on port 52773 to port 80 (for HTTP) or port 443 (HTTPS).

David Hockenbroch · Nov 8, 2023 go to post

Could you maybe use $LISTTOSTRING using a delimiter that works for you (something unlikely to be in your actual list data), convert that to uppercase, then use $LISTFROMSTRING to get it back into $LIST format with everything capitalized and not breaking the list?

David Hockenbroch · Nov 8, 2023 go to post

That's an issue with the SSL certificate on the server. It's the kind of error you'd see if the domains on the SSL certificate don't match the domain being used to access the website. SSL certificates are a little out of my areas of expertise, so I'm not sure how you determine what you need to do to fix that.

David Hockenbroch · Oct 26, 2023 go to post

Yes, security settings can get down to a columnar level. Also, if what you're querying is a view rather than a table, or if the ID column is defined to be something other than ID, that column just might not exist.

Also for clarification, when you use the select *, are you getting an Id column in the resulting data?

David Hockenbroch · Oct 26, 2023 go to post

I haven't touched PHP in a bit, but are you binding your columns? I seem to remember PHP things getting a little finicky about making sure you do that before you fetch if you were specifying columns in the query, but not if you were doing a select *.

David Hockenbroch · Oct 25, 2023 go to post

I didn't check the credentials because this all takes place after the user has already been through the authentication process. If they aren't valid, they wouldn't get this far anyway. My approach is dealing with just the authorization, not the authentication. I still use $SYSTEM.Security.Check to see if the process has the right permissions.

As you said, it comes down to personal preference. One of my preferences is not messing with the authentication processes if I don't have to. That way I don't have to account for all of the different authentication options, and they could all still be used.

David Hockenbroch · Oct 24, 2023 go to post

I was actually pondering this question myself for an article I'm working on. I ended up in a very different place than you did, though. I created an Abstract class that extends %CSP.REST and overrides the XData schema, the DispatchMap method, and the DispatchRequest method. If I extend this class - which I've called REST.Resourceful - I can include a resource in the URL map as a resource:permission pair. For example:

Will only allow you to access the /securetest endpoint if the user has Use permission on the resource "MyResource". If you don't, you get a 401 error. If I leave out the Resource attribute on that node, it doesn't check for any additional resources.

Source code is below. If you search the text for my initials, DLH, you'll see comments where I've made changes in the method, plus I added the attribute "Resource" to the XData schema.

/// Extends the %CSP.REST class to include securing of endpoints by resource in the URL map
Class REST.Resourceful Extends %CSP.REST [ Abstract ]
{ XData Schema [ Internal ]
{
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema> <xs:element name="Routes">
<xs:complexType>
<xs:choice  minOccurs="0" maxOccurs="unbounded">
<xs:element name="Route">
<xs:complexType>
<xs:attribute name="Url"    type="string" use="required"/>
<xs:attribute name="Method" type="string" use="required"/>
<xs:attribute name="Call" type="call" use="required"/>
<xs:attribute name="Cors" type="xs:boolean" use="optional" default="false"/>
<xs:attribute name="Resource" type="string" use="optional" default=" "/>
</xs:complexType>
</xs:element>
<xs:element name="Map">
<xs:complexType>
<xs:attribute name="Prefix" type="string" use="required"/>
<xs:attribute name="Forward" type="forward" use="required"/>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element> <xs:simpleType name="call">
<xs:restriction base="xs:string">
<xs:pattern value="([%]?[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*:)?[%]?[a-zA-Z][a-zA-Z0-9]*"/>
</xs:restriction>
</xs:simpleType> <xs:simpleType name="forward">
<xs:restriction base="xs:string">
<xs:pattern value="([%]?[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*)"/>
</xs:restriction>
</xs:simpleType> <xs:simpleType name="string">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType> </xs:schema>
} /// This generator creates the DispatchMap Method used to dispatch the Url and Method to the associated target method
ClassMethod DispatchMap(pIndex As %Integer) As %String [ CodeMode = generator ]
{
#dim tSC As %Status = $$$OK
    #dim As %Exception.AbstractException
    
    #dim tStream As %Stream.TmpCharacter
    #dim tHandler As %XML.ImportHandler
    #dim tCompiledClass As %Dictionary.CompiledClass
    
    #dim tArgCount,tIndex,tI,tCounter As %Integer
    #dim tArgs,tChild,tClassName,tCall,tCors,tForward,tError,tMap,tMethod,tPattern,tPiece,tPrefix,tType,tUrl,tResource As %String
    
    Try {
        
        Set tClassName=%classname
        
        #; Don't run on base class
        If tClassName="%CSP.REST" Quit
       
        #; Find named XDATA block
        If ##class(%Dictionary.CompiledXData).%ExistsId(tClassName_"||UrlMap") {
            
            Set tCompiledClass=##class(%Dictionary.CompiledClass).%OpenId(tClassName,,.tSC)
            If '$IsObject(tCompiledClass)||$$$ISERR(tSC) Quit
            
            Set tIndex = tCompiledClass.XDatas.FindObjectId(tClassName_"||UrlMap")
            If tIndex="" Set tSC=$$$ERROR($$$XDataBlockMissing,tClassName,"UrlMap") Quit
        
            #; Get XDATA as stream
            Set tStream = tCompiledClass.XDatas.GetAt(tIndex).Data
            Do tStream.Rewind()
            
            #; Create an XML import handler ( use the internal handler )
            Set tHandler=##class(%XML.ImportHandler).%New("CacheTemp",$$$IntHandler)
        
            #; Create the Entity Resolver
            Set tResolver=##class(%XML.SAX.XDataEntityResolver).%New(tClassName)
        
            #; Parse the XML data in the specfied stream
            Set tSC=##Class(%XML.SAX.Parser).ParseStream(tStream,tHandler,tResolver,,,"Schema")
            If $$$ISERR(tSC) Quit       
        
            #; Copy tree because handler will delete its copy when it goes out of scope
            Merge tMap=@tHandler.DOMName@(tHandler.Tree)
    
            If $Data(tMap("error"))||$Data(tMap("warning")) {
                
                Set tSC=$$$ERROR($$$InvalidDispatchMap)
                For tType="error","warning" {       
                    Set tIndex = "" For {
                        Set tIndex=$Order(tMap(tType,tIndex),1,tError) If tIndex="" Quit
                        Set tSC=$$$ADDSC(tSC,$$$ERROR($$$GeneralError,tError))
                    }
                }
                Quit
            }
            
            #; Walk the xml and generate the routing map
            Set tChild="",tCounter=0 For {
                
                Set tChild=$Order(tMap(1,"c",tChild)) If tChild="" Quit
                
                If tMap(tChild)="Route" {
                    
                #; Need to substitute capture groups for arguments
                #; Added setting of tResource based on URL map - DLH
                Set tPattern="",tArgCount=0,tUrl=tMap(tChild,"a","Url"),tCors=tMap(tChild,"a","Cors"),tResource=tMap(tChild,"a","Resource")
                        
                #; Substitute variable placeholders for capture group
                For tI=1:1:$Length(tUrl,"/") {
                    Set tPiece=$Piece(tUrl,"/",tI)
                    If $Extract(tPiece)=":" {
                        Set $Piece(tPattern,"/",tI)="([^"_$Char(0)_"]+)"
                    else {
                        Set $Piece(tPattern,"/",tI)=tPiece                  }
                }
                Set tPattern=$Translate(tPattern,$Char(0),"/")                 Set tCounter=$Increment(tCounter),tMethod=tMap(tChild,"a","Method"),tCall=$Get(tMap(tChild,"a","Call"))
                #; Added getting resource from the URL Map here. - DLH
                $$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""R"","""_tPattern_""","""_tMethod_""","""_tCall_""","""_tCors_""","""_tResource_""")")
                
            else {
                
                Set tCounter=$Increment(tCounter),tPrefix=tMap(tChild,"a","Prefix"),tForward=$Get(tMap(tChild,"a","Forward"))                 #; Need to substitute capture groups for arguments
                Set tPattern=""
                For tI=2:1:$Length(tPrefix,"/") {
                    Set tPiece=$Piece(tPrefix,"/",tI)
                    If $Extract(tPiece)=":" {
                        Set tPattern=tPattern_"/[^/]+"
                    else {
                        Set tPattern=tPattern_"/"_tPiece
                    }
                }
                
                Set tPattern = "("_ tPattern _ ")/.*"
                
                $$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""M"","""_tPattern_""","""_tForward_""")")
            }
            }
            $$$GENERATE(" Quit """"")
                
        else {
            
            #; The specified class must have an XDATA Block named UrlMap
            Set tSC=$$$ERROR($$$XDataBlockMissing,tClassName,"UrlMap")
        }
        
    Catch (e) {
        Set tSC=e.AsStatus()
    }
    
    Quit tSC
} /// Dispatch a REST request according to URL and Method
ClassMethod DispatchRequest(pUrl As %String, pMethod As %String, pForwarded As %Boolean = 0) As %Status
{
    #dim tSC As %Status = $$$OK
    #dim As %Exception.AbstractException
    
    #dim tMatcher As %Regex.Matcher
    
    #dim tArgs,tClass,tMatchUrl,tMapEntry,tRegEx,tCall,tForward,tAccess,tSupportedVerbs,tTarget,tType As %String
    #dim tI,tIndex As %Integer
    #dim tResourceMatched,tContinue As %Boolean
    #dim tMethodMatched As %Boolean
    
    Try {
        
        Set (tResourceMatched,tMethodMatched)=0
        #; Initializing tSecurityResourceMatched - DLH
        Set tSecurityResourceMatched = 1
        #; Extract the match url from the application name
        If (0=pForwarded) {
            Set tMatchUrl="/"_$Extract(pUrl,$Length(%request.Application)+1,*)
        else {
            Set tMatchUrl=pUrl
        }
      
        #; Uppercase the method
        Set pMethod=$ZCVT(pMethod,"U")
          
        #; Pre-Dispatch
        Set tContinue=1,tSC=..OnPreDispatch(tMatchUrl,pMethod,.tContinue)
        If $$$ISERR(tSC) Quit
        
        #; It's the users responsibility to return the response in OnPreDispatch() if Continue = 0
        If tContinue=0 Quit
            
        #; Walk the dispatch map in collation order of defintion
        For tIndex=1:1 {
            
            #; Get the next map entry
            Set tMapEntry=..DispatchMap(tIndex) If tMapEntry="" Quit
             
            #; Pick out the RegEx
            Set tRegEx=$List(tMapEntry,2)
            
            #; Create a matcher
            Set tMatcher=##class(%Regex.Matcher).%New(tRegEx)
            
            #; Test each regular expression in turn, extracting the arguments,
            #; dispatching to the named method
            If tMatcher.Match(tMatchUrl) {
#; We have matched the resource
                Set tResourceMatched=1
                #; Logic to check the resource from the URL map
                set tResource = $List(tMapEntry,6)
                If tResource '= " "{
                If $SYSTEM.Security.Check($P(tResource,":",1),$P(tResource,":",2))=0{
         Set tSecurityResourceMatched=0
         }
                }
  #; Added an if so the method only gets dispatched if we have the resource permission
                If tSecurityResourceMatched = 1{
                
                Set tType=$List(tMapEntry,1)
                 
                 #; If we are a simple route
                 If tType="R" {
                    
                     #; Support OPTIONS VERB (cannot be overriden)
                    If pMethod="OPTIONS" {
                         
                         Set tMethodMatched=1
                        
                         Set tSC=..OnHandleOptionsRequest(tMatchUrl)
                         If $$$ISERR(tSC) Quit
                        
                         #; Dispatch CORS
                        Set tSC=..ProcessCorsRequest(pUrl,$list(tMapEntry,5))
                         If $$$ISERR(tSC) Quit
                        
                         Quit
                     }
                     
                     #; comparison is case-insensitive now
                    If pMethod'=$ZCVT($List(tMapEntry,3),"U") Continue
                     
                     Set tTarget=$List(tMapEntry,4)
                     
                     #; We have matched a method
                     Set tMethodMatched=1
                    
                    #; Dispatch CORS
                     Set tSC=..ProcessCorsRequest(pUrl,$list(tMapEntry,5))
                     If $$$ISERR(tSC) Quit
                   
                     #; Got a match, marshall the arguments can call directly
                     If tMatcher.GroupCount {
                         For tI=1:1:tMatcher.GroupCount Set tArgs(tI)=tMatcher.Group(tI)
                         Set tArgs=tI
                     else {
                         Set tArgs=0
                     }
                    
                     #; Check for optional ClassName prefix
                     Set tClass=$classname()
                     If tTarget[":" Set tClass=$Piece(tTarget,":"),tTarget=$Piece(tTarget,":",2)
                    
                     #; Dispatch
                     Set tSC=$zobjclassmethod(tClass,tTarget,tArgs...)
                       
                 else {
                    
                     #; We are a map, massage the URL and forward the request
                     Set tMatchUrl=$piece(tMatchUrl,tMatcher.Group(1),"2",*),tForward=$ListGet(tMapEntry,3)
                     Set (tResourceMatched,tMethodMatched)=1
                   
                     #; Dispatch with modified URL
                     Set tSC=$zobjclassmethod(tForward,"DispatchRequest",tMatchUrl,pMethod,1)
                 }
                
                 If $$$ISERR(tSC) Quit
                
                 #; Don't want multiple matches
                 Quit
                }
            }
        }
        
        #; Didn't have permission for the resource required by this enpoint; return 401 - DLH
        If tSecurityResourceMatched = 0 Set tSC=..ReportHttpStatusCode(..#HTTP401UNAUTHORIZED) Quit
        
        #; Didn't have a match for the resource, report not found
        If tResourceMatched=0 Set tSC=..ReportHttpStatusCode(..#HTTP404NOTFOUND) Quit
                  
        #; Had a match for resource but method not matched
        If tMethodMatched=0 {
            
            Set tSC=..SupportedVerbs(tMatchUrl,.tSupportedVerbs)
            If $$$ISERR(tSC) Quit
            
            Set tSC=..Http405(tSupportedVerbs) Quit
        }
            
    Catch (e) {
        
        Set tSC=e.AsStatus()
    }
    
    Quit tSC
} }
 
David Hockenbroch · Oct 10, 2023 go to post

I don't typically use Ensemble for my REST services, but I do see a couple of things.

First, in the documentation you linked to, pOutput is a %DynamicObject, not a %RegisteredObject. %RegisteredObject does not have a %ToJSON() method.

Second, in your PostMessage method, the line "return pResponse" shouldn't be there.

David Hockenbroch · Sep 29, 2023 go to post

Where you have your query defined, does it work if you change the definition to include the SELECTMODE as RUNTIME?

Query GetAddressQuery() As %SQLQuery(ROWSPEC = "Address",SELECTMODE = "RUNTIME") [ SqlProc ]
David Hockenbroch · Sep 22, 2023 go to post

Just to clarify, Tony, I'm not looking for a notification that a failover has occurred. I'm looking for a notification that the async mirror has gone offline.

David Hockenbroch · Sep 14, 2023 go to post

What browsers are you using to do this? Many have restricted the use of window.close to only work on windows that were also opened by javascript using window.open.

David Hockenbroch · Sep 13, 2023 go to post

Do so at your own risk, but if you use _PUBLIC as the role/user argument, then all users will have the granted permissions as soon as they are created.