lib/jss/api_object/script.rb in ruby-jss-1.5.3 vs lib/jss/api_object/script.rb in ruby-jss-1.6.0b1

- old
+ new

@@ -1,96 +1,93 @@ -### Copyright 2020 Pixar +# Copyright 2020 Pixar -### -### Licensed under the Apache License, Version 2.0 (the "Apache License") -### with the following modification; you may not use this file except in -### compliance with the Apache License and the following modification to it: -### Section 6. Trademarks. is deleted and replaced with: -### -### 6. Trademarks. This License does not grant permission to use the trade -### names, trademarks, service marks, or product names of the Licensor -### and its affiliates, except as required to comply with Section 4(c) of -### the License and to reproduce the content of the NOTICE file. -### -### You may obtain a copy of the Apache License at -### -### http://www.apache.org/licenses/LICENSE-2.0 -### -### Unless required by applicable law or agreed to in writing, software -### distributed under the Apache License with the above modification is -### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -### KIND, either express or implied. See the Apache License for the specific -### language governing permissions and limitations under the Apache License. -### -### +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. +# +# -### +# module JSS - ### Module Constants + # Module Constants ##################################### - ### Module Variables + # Module Variables ##################################### - ### Module Methods + # Module Methods ##################################### - ### Classes + # Classes ##################################### - ### A Script in the JSS. - ### - ### As of Casper 9.4, the script contents as stored in the database are - ### accessible via the API - ### - ### This class will save the script contents back to the database with - ### the {#create} or {#update} methods - ### - ### If your scripts are stored on the master distribution point instead of - ### the database, you can use {#upload_master_file} to save it to the server, - ### and {#delete_master_file} to delete it from the server. - ### - ### Use the {#run} method to run the script on the local machine via the 'jamf runScript' command - ### - ### @see JSS::APIObject - ### + # A Script in the JSS. + # + # As of Casper 9.4, the script contents as stored in the database are + # accessible via the API + # + # According to Jamf as of early 2021, it has been some years now since + # its been possible to store script contents on a dist. point - they + # are all always in the database. + # + # Use the {#run} method to run the script on the local machine. + # + # @see JSS::APIObject + # class Script < JSS::APIObject - ### Mix-Ins + # Mix-Ins ##################################### include JSS::Creatable include JSS::Updatable include JSS::Categorizable - ### Class Methods + # Class Methods ##################################### - ### Class Constants + # Class Constants ##################################### - ### The base for REST resources of this class + # The base for REST resources of this class RSRC_BASE = 'scripts'.freeze - ### the hash key used for the JSON list output of all objects in the JSS + # the hash key used for the JSON list output of all objects in the JSS RSRC_LIST_KEY = :scripts - ### The hash key used for the JSON object output. - ### It's also used in various error messages + # The hash key used for the JSON object output. + # It's also used in various error messages RSRC_OBJECT_KEY = :script - ### The script storage folder on the distribution point, if used + # The script storage folder on the distribution point, if used DIST_POINT_SCRIPTS_FOLDER = 'Scripts'.freeze - ### Priority to use for running the script in relation to other actions during imaging + # Priority to use for running the script in relation to other actions during imaging PRIORITIES = ['Before', 'After', 'At Reboot'].freeze - ### which is default? + # which is default? DEFAULT_PRIORITY = 'After'.freeze - ### The keys used in the @parameters Hash + # The keys used in the @parameters Hash PARAMETER_KEYS = [:parameter4, :parameter5, :parameter6, :parameter7, :parameter8, :parameter9, :parameter10, :parameter11].freeze # the object type for this object in # the object history table. # See {APIObject#add_object_history_entry} @@ -101,119 +98,128 @@ # How is the category stored in the API data? CATEGORY_DATA_TYPE = String - ### Attributes + # Attributes ##################################### - ### @return [String] the file name of the script, if stored in a dist. point + # @return [String] the file name of the script, if stored in a dist. point attr_reader :filename - ### @return [Array<String>] the OS versions this can be installed onto. For all minor versions, the format is 10.5.x + # @return [Array<String>] the OS versions this can be installed onto. For all minor versions, the format is 10.5.x attr_reader :os_requirements - ### @return [String] either 'Before' or 'After' or "At Reboot". + # @return [String] either 'Before' or 'After' or "At Reboot". attr_reader :priority - ### @return [String] the info field for this script + # @return [String] the info field for this script attr_reader :info - ### @return [String] the notes field for this script + # @return [String] the notes field for this script attr_reader :notes - ### @return [Hash] script parameters 4-11. Parameters 1-3 are predefined as target drive, computer name, and username + # @return [Hash] descriptions of parameters 4-11. Parameters 1-3 are predefined as target drive, computer name, and username attr_reader :parameters + alias parameter_labels parameters + alias parameter_descriptions parameters - ### @return {String] the actual code for this script, if it's stored in the database. + # @return {String] the actual code for this script, if it's stored in the database. attr_reader :script_contents - ### Constructor + # @return [String] the code for this script, Base64-encoded + attr_reader :script_contents_encoded + + # Constructor ##################################### - ### + # def initialize(args = {}) super @filename = @init_data[:filename] || @name @info = @init_data[:info] @notes = @init_data[:notes] @os_requirements = @init_data[:os_requirements] ? JSS.to_s_and_a(@init_data[:os_requirements])[:arrayform] : [] @parameters = @init_data[:parameters] ? @init_data[:parameters] : {} @priority = @init_data[:priority] || DEFAULT_PRIORITY @script_contents = @init_data[:script_contents] + @script_contents_encoded = @init_data[:script_contents_encoded] + if @script_contents && @script_contents_encoded.to_s.empty? + @script_contents_encoded = Base64.encode64 @script_contents + end end # initialize - ### Change the script filename - ### - ### Setting it to nil will make it match the script name - ### - ### @param new_val[String,Nil] the new filename - ### - ### @return [void] - ### - ### @note This method does NOT change the filename on the distribution point - ### if that's where you store your scripts. - ### + # Change the script filename + # + # Setting it to nil will make it match the script name + # + # @param new_val[String,Nil] the new filename + # + # @return [void] + # + # @note This method does NOT change the filename on the distribution point + # if that's where you store your scripts. + # def filename=(new_val) new_val = nil if new_val == '' new_val = @name unless new_val return nil if new_val == @filename @filename = new_val @need_to_update = true end # filename= - ### Change the script's display name - ### - ### If the filename is the same as the name, the filename will be changed also - ### - ### @param new_val[String] the new display name - ### - ### @return [void] - ### + # Change the script's display name + # + # If the filename is the same as the name, the filename will be changed also + # + # @param new_val[String] the new display name + # + # @return [void] + # def name=(new_val) new_val = new_val.to_s return if new_val == @name raise JSS::MissingDataError, "Name can't be empty" if new_val.empty? raise JSS::AlreadyExistsError, "A script already exists with the name '#{new_val}'" if JSS::Script.all_names.include? new_val - ### if the filename matches the name, change that too. + # if the filename matches the name, change that too. @filename = new_val if @filename == @name @name = new_val - ### if our REST resource is based on the name, update that too + # if our REST resource is based on the name, update that too @rest_rsrc = "#{RSRC_BASE}/name/#{CGI.escape @name.to_s}" if @rest_rsrc.include? '/name/' @need_to_update = true end # name= - ### Change the os_requirements - ### - ### Minumum OS's can be specified as a string using the notation ">=10.6.7" - ### See the {JSS.expand_min_os} method for details. - ### - ### @param new_val[String, Array<String>] the new os requirements as a comma-separted String or an Array of Strings - ### - ### @return [void] - ### - ### @example String value - ### myscript.os_requirements "10.5, 10.5.3, 10.6.x" - ### - ### @example Array value - ### ok_oses = ['10.5', '10.5.3', '10.6.x'] - ### myscript.os_requirements ok_oses - ### - ### @example Minimum OS - ### myscript.os_requirements ">=10.7.5" - ### + # Change the os_requirements + # + # Minumum OS's can be specified as a string using the notation ">=10.6.7" + # See the {JSS.expand_min_os} method for details. + # + # @param new_val[String, Array<String>] the new os requirements as a comma-separted String or an Array of Strings + # + # @return [void] + # + # @example String value + # myscript.os_requirements "10.5, 10.5.3, 10.6.x" + # + # @example Array value + # ok_oses = ['10.5', '10.5.3', '10.6.x'] + # myscript.os_requirements ok_oses + # + # @example Minimum OS + # myscript.os_requirements ">=10.7.5" + # def os_requirements=(new_val) - ### nil should be an empty array + # nil should be an empty array new_val = [] if new_val.to_s.empty? - ### if any value starts with >=, expand it + # if any value starts with >=, expand it case new_val when String new_val = JSS.expand_min_os(new_val) if new_val =~ /^>=/ when Array new_val.map! { |a| a =~ /^>=/ ? JSS.expand_min_os(a) : a } @@ -221,111 +227,113 @@ new_val.uniq! else raise JSS::InvalidDataError, 'os_requirements must be a String or an Array of strings' end # case - ### get the array version + # get the array version @os_requirements = JSS.to_s_and_a(new_val)[:arrayform] @need_to_update = true end # os_requirements= - ### Change the priority of this script - ### - ### @param new_val[Integer] the new priority, which must be one of {PRIORITIES} - ### - ### @return [void] - ### + # Change the priority of this script + # + # @param new_val[Integer] the new priority, which must be one of {PRIORITIES} + # + # @return [void] + # def priority=(new_val) return nil if new_val == @priority new_val = DEFAULT_PRIORITY if new_val.nil? || (new_val == '') raise JSS::InvalidDataError, ":priority must be one of: #{PRIORITIES.join ', '}" unless PRIORITIES.include? new_val @priority = new_val @need_to_update = true end # priority= - ### Change the info field - ### - ### @param new_val[String] the new info - ### - ### @return [void] - ### + # Change the info field + # + # @param new_val[String] the new info + # + # @return [void] + # def info=(new_val) return nil if new_val == @info - ### line breaks should be \r + # line breaks should be \r new_val = new_val.to_s.tr("\n", "\r") @info = new_val @need_to_update = true end # info= - ### Change the notes field - ### - ### @param new_val[String] the new notes - ### - ### @return [void] - ### + # Change the notes field + # + # @param new_val[String] the new notes + # + # @return [void] + # def notes=(new_val) return nil if new_val == @notes - ### line breaks should be \r + # line breaks should be \r new_val = new_val.to_s.tr("\n", "\r") @notes = new_val @need_to_update = true end # notes= - ### Replace all the script parameters at once. - ### - ### This will replace the entire set with the hash provided. - ### - ### @param new_val[Hash] the Hash keys must exist in {PARAMETER_KEYS} - ### - ### @return [void] - ### + # Replace all the script parameters at once. + # + # This will replace the entire set with the hash provided. + # + # @param new_val[Hash] the Hash keys must exist in {PARAMETER_KEYS} + # + # @return [void] + # def parameters=(new_val) return nil if new_val == @parameters new_val = {} if new_val.nil? || (new_val == '') - ### check the values + # check the values raise JSS::InvalidDataError, ':parameters must be a Hash with keys :parameter4 thru :parameter11' unless \ new_val.is_a?(Hash) && ((new_val.keys & PARAMETER_KEYS) == new_val.keys) new_val.each do |_k, v| raise JSS::InvalidDataError, ':parameter values must be strings or nil' unless v.nil? || v.is_a?(String) end @parameters = new_val @need_to_update = true end # parameters= - ### Change one of the stored parameters - ### - ### @param param_num[Integer] which param are we setting? must be 4..11 - ### - ### @param new_val[String] the new value for the parameter - ### - ### @return [void] - ### + # Change one of the stored parameters + # + # @param param_num[Integer] which param are we setting? must be 4..11 + # + # @param new_val[String] the new value for the parameter + # + # @return [void] + # def set_parameter(param_num, new_val) raise JSS::NoSuchItemError, 'Parameter numbers must be from 4-11' unless (4..11).cover? param_num pkey = "parameter#{param_num}".to_sym raise JSS::InvalidDataError, 'parameter values must be strings or nil' unless new_val.nil? || new_val.is_a?(String) return nil if new_val == @parameters[pkey] @parameters[pkey] = new_val @need_to_update = true end + alias set_parameter_label set_parameter + alias set_parameter_description set_parameter - ### Change the executable code of this script. - ### - ### If the arg is a Pathname instance, or a String starting with "/" - ### Then the arg is assumed to be a file from which to read the code. - ### - ### Otherwise it should be a String with the code itself, and it must start with '#!" - ### - ### After doing this, use {#create} or {#update} to write it to the database or - ### use {#upload_master_file} to save it to the master dist. point. - ### - ### @param new_val[String,Pathname] the new script contents or a path to a file containing it. - ### - ### @return [void] - ### + # Change the executable code of this script. + # + # If the arg is a Pathname instance, or a String starting with "/" + # Then the arg is assumed to be a file from which to read the code. + # + # Otherwise it should be a String with the code itself, and it must start with '#!" + # + # After doing this, use {#create} or {#update} to write it to the database or + # use {#upload_master_file} to save it to the master dist. point. + # + # @param new_val[String,Pathname] the new script contents or a path to a file containing it. + # + # @return [void] + # def script_contents=(new_val) new_code = case new_val when String if new_val.start_with? '/' Pathname.new(new_val).read @@ -339,228 +347,110 @@ end # case raise JSS::InvalidDataError, "Script contents must start with '#!'" unless new_code.start_with? '#!' @script_contents = new_code + @script_contents_encoded = Base64.encode64 @script_contents @need_to_update = true end - ### Save the @script_contents for this script to a file on the Master Distribution point. - ### - ### If you'll be uploading several files you can specify unmount as false, and do it manually when all - ### are finished. - ### - ### use {#script_contents=} to set the script_contents from a String or Pathname - ### - ### @param rw_pw[String] the password for the read/write account on the master Distribution Point - ### - ### @param unmount[Boolean] whether or not ot unount the distribution point when finished. - ### - ### @return [void] - ### - def upload_master_file(rw_pw, unmount = true) - raise JSS::MissingDataError, 'No code specified. Use #code= first.' if @script_contents.nil? || @script_contents.empty? - - mdp = JSS::DistributionPoint.master_distribution_point - raise JSS::InvaldDatatError, 'Incorrect password for read-write access to master distribution point.' unless mdp.check_pw :rw, rw_pw - - destination = mdp.mount(rw_pw, :rw) + "#{DIST_POINT_SCRIPTS_FOLDER}/#{@filename}" - destination.save @script_contents - mdp.unmount if unmount - end # upload - - ### Delete the filename from the master distribution point, if it exists. - ### - ### If you'll be uploading several files you can specify unmount as false, and do it manually when all - ### are finished. - ### - ### @param rw_pw[String] the password for the read/write account on the master Distribution Point - ### - ### @param unmount[Boolean] whether or not ot unount the distribution point when finished. - ### - ### @return [Boolean] was the file deleted? - ### - def delete_master_file(rw_pw, unmount = true) - file = JSS::DistributionPoint.master_distribution_point.mount(rw_pw, :rw) + "#{DIST_POINT_SCRIPTS_FOLDER}/#{@filename}" - if file.exist? - file.delete - did_it = true - else - did_it = false - end # if exists - JSS::DistributionPoint.master_distribution_point.unmount if unmount - did_it - end - - ### Run this script on the current machine using the "jamf runScript" command. - ### - ### If the script code is available in the {#script_contents} attribute, then that - ### code is saved to a tmp file, and executed. Otherwise, the script is assumed - ### to be stored on the distribution point. - ### - ### If the dist. point has http downloads enabled, then the URL is used as the path with the - ### 'jamf runScript' command. - ### - ### If http is not an option, the dist.point is mounted, and the script copied locally before running. - ### In this case the options must include :ro_pw => 'somepass' - ### to provide the read-only password for mounting the distribution point. If :unmount => true - ### is provided, the dist. point will be unmounted immediately after copying - ### the script locally. Otherwise it will remain mounted, in case there's further need of it. - ### - ### Any local on-disk copies of the script are removed after running. - ### - ### After the script runs, this method returns a two-item Array. - ### - the first item is an Integer, the exit status of the script itself (0 means success) - ### - the second item is a String, the output (stdout + stderr) of the jamf binary, which will include - ### the script output. - ### The exit status of the jamf binary process will be available as a Process::Status object - ### in $? immediately after running. - ### - ### @param opts[Hash] the options for running the script - ### - ### @option opts :target[String,Pathname] the 'target drive', passed to the script as the first commandline option. - ### Defaults to '/' - ### - ### @option opts :computer_name[String] the name of the computer, passed to the script as the second commandline - ### option. Defaults to the name of the current machine - ### - ### @option opts :username[String] the username to be passed to the script as the third commandline option. - ### - ### @option opts :p1..:p8[String] the values to be passed as the 4th - 11th commandline options, overriding - ### those defined with the script in the JSS - ### - ### @option opts :ro_pw[String] the read-only password for mounting the distribution point, if needed - ### - ### @option opts :unmount[Boolean} should the dist. point be unmounted when finished, if we mounted it? - ### - ### @option opts :verbose[Boolean] should the 'jamf runScript' command be verbose? - ### - ### @option opts :show_output[Boolean] should the output (stdout + stderr) of 'jamf runScript' be copied to - ### stdout in realtime, as well as returned? - ### - ### @return [Array<(Integer,String)>] the exit status of the *script* and stdout+stderr of 'jamf runScript'. - ### The exit status of the jamf binary will be available in $? immediately after running. - ### - ### *NOTE* In the WEB UI and API, the definable parameters are numbered 4-11, since 1, 2, & 3 are the - ### target drive, computer name, and user name respectively. However, the jamf binary refers to them as - ### p1-p8, and that's how they are expected as options to #run. So if :p1=> "new param" is given as an - ### aption to #run, it will override any value that the API provided in @parameters[:parameter4] - ### + # Run this script on the current machine. + # + # If the script code is available in the {#script_contents} attribute, then that + # code is saved to a tmp file, and executed. The tmp file is deleted immediately + # after running + # + # After the script runs, this method returns a two-item Array. + # - the first item is an Integer, the exit status of the script itself (0 means success) + # - the second item is a String, the output (stdout + stderr) of the script. + # + # The exit status of the jamf binary process will be available as a Process::Status object + # in $? immediately after running. + # + # @param opts[Hash] the options for running the script + # + # @option opts :target[String,Pathname] the 'target drive', passed to the script as the first commandline option. + # Defaults to '/' + # + # @option opts :computer_name[String] the name of the computer, passed to the script as the second commandline + # option. Defaults to the name of the current machine + # + # @option opts :username[String] the username to be passed to the script as the third commandline option. + # Defaults to the current console user. + # + # @option opts :p4..:p11[String] the values to be passed as the 4th - 11th commandline params + # Script params 1, 2, & 3 are the target:, computer_name: and username: params + # + # @option opts :show_output[Boolean] should the output (stdout + stderr) be copied to + # stdout in realtime, as well as returned? + # + # @return [Array<(Integer,String)>] the exit status and stdout+stderr of the script + # def run(opts = {}) + raise JSS::MissingDataError, 'script_contents does not start with #!' unless @script_contents.to_s.start_with? '#!' + opts[:target] ||= '/' - opts[:p1] ||= @parameters[:parameter4] - opts[:p2] ||= @parameters[:parameter5] - opts[:p3] ||= @parameters[:parameter6] - opts[:p4] ||= @parameters[:parameter7] - opts[:p5] ||= @parameters[:parameter8] - opts[:p6] ||= @parameters[:parameter9] - opts[:p7] ||= @parameters[:parameter10] - opts[:p8] ||= @parameters[:parameter11] + opts[:computer_name] ||= JSS::Client.run_jamf('getComputerName')[/>(.)</, 1] + opts[:username] ||= JSS::Client.console_user - dp_mount_pt = nil - delete_exec = false + params = [opts[:target], opts[:computer_name], opts[:username]] + params << opts[:p4] + params << opts[:p5] + params << opts[:p6] + params << opts[:p7] + params << opts[:p8] + params << opts[:p9] + params << opts[:p10] + params << opts[:p11] - begin + # everything must be a string + params.map! &:to_s - # do we have the code already? if so, save it out and make it executable - if @script_contents && !@script_contents.empty? + # remove nils + params.compact! - script_path = JSS::Client::DOWNLOADS_FOLDER + # remove empty strings + params.delete_if &:empty? - executable = script_path + @filename + return_value = [] - executable.jss_touch - executable.chmod 0o700 - executable.jss_save @script_contents - delete_exec = true + # Save and run the script from a private temp dir + # which will be deleted when finished + require 'tmpdir' + Dir.mktmpdir do |dir| + executable = Pathname.new "#{dir}/#{@name}" + executable.jss_touch + executable.chmod 0o700 + executable.jss_save @script_contents - # otherwise, get it from the dist. point - else - dist_point = JSS::DistributionPoint.my_distribution_point api: @api + cmd = [executable.to_s] + cmd += params - ### how do we access our dist. point? - if dist_point.http_downloads_enabled - script_path = dist_point.http_url + "/#{DIST_POINT_SCRIPTS_FOLDER}/" + stdout_and_stderr_str, status = Open3.capture2e(*cmd) - else - dp_mount_pt = dist_point.mount opts[:ro_pw] + return_value << status.exitstatus + return_value << stdout_and_stderr_str + end # Dir.mktmpdirs - script_path = (dp_mount_pt + DIST_POINT_SCRIPTS_FOLDER) - - end # if http enabled - - end # if @script_contents and (not @script_contents.empty?) - - # build the command as an array. - command_arry = ['-script', @filename, '-path', script_path.to_s] - - command_arry << '-target' - command_arry << opts[:target].to_s - - command_arry << '-computerName' if opts[:computer_name] - command_arry << opts[:computer_name] if opts[:computer_name] - - command_arry << '-username' if opts[:username] - command_arry << opts[:username] if opts[:username] - - command_arry << '-p1' if opts[:p1] - command_arry << opts[:p1] if opts[:p1] - - command_arry << '-p2' if opts[:p2] - command_arry << opts[:p2] if opts[:p2] - - command_arry << '-p3' if opts[:p3] - command_arry << opts[:p3] if opts[:p3] - - command_arry << '-p4' if opts[:p4] - command_arry << opts[:p4] if opts[:p4] - - command_arry << '-p5' if opts[:p5] - command_arry << opts[:p5] if opts[:p5] - - command_arry << '-p6' if opts[:p6] - command_arry << opts[:p6] if opts[:p6] - - command_arry << '-p7' if opts[:p7] - command_arry << opts[:p7] if opts[:p7] - - command_arry << '-p8' if opts[:p8] - command_arry << opts[:p8] if opts[:p8] - - command_arry << '-verbose' if opts[:verbose] - - command = command_arry.shelljoin - - jamf_output = JSS::Client.run_jamf 'runScript', command, opts[:show_output] - - jamf_output =~ /^.*Script exit code: (\d+)(\D|$)/ - - script_exitstatus = Regexp.last_match(1).to_i - - ensure - executable.delete if delete_exec && executable.exist? - dist_point.unmount if dp_mount_pt && dp_mount_pt.mountpoint? && opts[:unmount] - end # begin/ensure - - [script_exitstatus, jamf_output] + return_value end # def run # aliases under their methods seem to confuse the YARD documenter, so I'm putting them all here. alias oses os_requirements alias oses= os_requirements= alias code script_contents alias code= script_contents= alias contents script_contents alias contents= script_contents= - ### Private Instance Methods + # Private Instance Methods ##################################### private - ### Return the xml for creating or updating this script in the JSS - ### + # Return the xml for creating or updating this script in the JSS + # def rest_xml doc = REXML::Document.new scpt = doc.add_element 'script' scpt.add_element('filename').text = @filename @@ -577,10 +467,10 @@ else pars = scpt.add_element('parameters') PARAMETER_KEYS.each { |p| pars.add_element(p.to_s).text = @parameters[p] } end - scpt.add_element('script_contents_encoded').text = Base64.encode64(@script_contents) + scpt.add_element('script_contents_encoded').text = script_contents_encoded doc.to_s end # rest xml end # class Script