note date: "$Date$" revision: "$Revision$" deferred class IRON_NODE_API_HANDLER inherit WSF_HANDLER WSF_SELF_DOCUMENTED_HANDLER feature -- Change set_iron (i: like iron) do iron := i end feature -- Access iron: IRON_NODE database: IRON_NODE_DATABASE do Result := iron.database end feature -- Access is_authenticated (req: WSF_REQUEST): BOOLEAN do Result := current_user (req) /= Void end current_user (req: WSF_REQUEST): detachable IRON_NODE_USER local l_auth: HTTP_AUTHORIZATION do if req.has_execution_variable ("{IRON_REPO}.user") then if attached {like current_user} req.execution_variable ("{IRON_REPO}.user") as u then Result := u end elseif attached req.http_authorization as l_http_authorization then create l_auth.make (l_http_authorization) if attached l_auth.is_basic and then attached l_auth.login as u and then attached l_auth.password as p and then iron.database.is_valid_credential (u, p) and then attached iron.database.user (u) as l_user then req.set_execution_variable ("{IRON_REPO}.user", l_user) Result := l_user end end end has_iron_version (req: WSF_REQUEST): BOOLEAN do Result := attached {WSF_STRING} req.path_parameter ("version") as p_version and then not p_version.is_empty end iron_version (req: WSF_REQUEST): IRON_NODE_VERSION do if attached {WSF_STRING} req.path_parameter ("version") as p_version and then attached p_version.value as v and then v.is_valid_as_string_8 then create Result.make (v.to_string_8) else create Result.make_default end end has_permission_to_modify_package (req: WSF_REQUEST; a_package: IRON_NODE_PACKAGE): BOOLEAN do if attached current_user (req) as u then Result := user_has_permission_to_modify_package (u, a_package) end end has_permission_to_modify_package_version (req: WSF_REQUEST; a_package: IRON_NODE_VERSION_PACKAGE): BOOLEAN do if attached current_user (req) as u then Result := user_has_permission_to_modify_package_version (u, a_package) end end user_has_permission_to_modify_package (a_user: IRON_NODE_USER; a_package: IRON_NODE_PACKAGE): BOOLEAN do if attached a_package.owner as o then Result := a_user.same_user (o) or else a_user.is_administrator else Result := a_user.is_administrator end end user_has_permission_to_modify_package_version (a_user: IRON_NODE_USER; a_package: IRON_NODE_VERSION_PACKAGE): BOOLEAN do if attached a_package.owner as o then Result := a_user.same_user (o) or else a_user.is_administrator else Result := a_user.is_administrator end end feature -- Request: methods method_query_parameter: STRING = "_method" is_method_get (req: WSF_REQUEST): BOOLEAN do Result := req.is_get_request_method end is_method_delete (req: WSF_REQUEST): BOOLEAN do Result := req.is_request_method ({HTTP_REQUEST_METHODS}.method_delete) or else ( req.is_get_request_method and then attached req.query_parameter (method_query_parameter) as m and then m.is_case_insensitive_equal ("delete") ) end is_method_post (req: WSF_REQUEST): BOOLEAN do Result := req.is_post_request_method end is_method_put (req: WSF_REQUEST): BOOLEAN do Result := req.is_request_method ({HTTP_REQUEST_METHODS}.method_put) or else ( req.is_post_request_method and then attached req.query_parameter (method_query_parameter) as m and then m.is_case_insensitive_equal ("put") ) end feature -- Request: accepted content type accepted_content_type_parameter: STRING = "_type" is_content_type_text_html_accepted (req: WSF_REQUEST): BOOLEAN do Result := req.is_content_type_accepted ("text/html") or else ( attached req.query_parameter (accepted_content_type_parameter) as m and then m.is_case_insensitive_equal ("html") ) end is_content_type_application_json_accepted (req: WSF_REQUEST): BOOLEAN do Result := req.is_content_type_accepted ("application/json") or else ( attached req.query_parameter (accepted_content_type_parameter) as m and then m.is_case_insensitive_equal ("json") ) end feature -- Redirection redirect_to_package_version (req: WSF_REQUEST; res: WSF_RESPONSE; a_package: IRON_NODE_VERSION_PACKAGE) local m: IRON_NODE_API_RESPONSE do create m.make (req, iron) m.set_location (req.absolute_script_url (iron.package_version_view_resource (a_package))) res.send (m) end feature -- Download download (a_url: READABLE_STRING_8; cl_path: CELL [detachable PATH]; req: WSF_REQUEST) local cl: LIBCURL_HTTP_CLIENT ctx: HTTP_CLIENT_REQUEST_CONTEXT f: detachable FILE do if attached current_user (req) as l_user then create cl.make if attached cl.new_session (a_url) as l_sess and then l_sess.is_available then create ctx.make l_sess.set_max_redirects (-1) l_sess.set_is_insecure (True) f := new_temporary_output_file ("tmp-download-" + l_user.name) if f /= Void and then f.is_open_write then ctx.set_output_content_file (f) if attached l_sess.get ("", ctx) as resp then cl_path.replace (f.path) end f.close end end end end new_temporary_output_file (n: detachable READABLE_STRING_8): detachable FILE local bp: detachable PATH d: DIRECTORY i: INTEGER do bp := iron.layout.tmp_path create d.make_with_path (bp) if not d.exists then d.recursive_create_dir end if n /= Void then bp := bp.extended ("tmp-download-" + n) else bp := bp.extended ("tmp") end from i := 0 until Result /= Void or i > 100 loop i := i + 1 create {RAW_FILE} Result.make_with_path (bp.appended ("__" + i.out)) if Result.exists then Result := Void else Result.open_write end end ensure Result /= Void implies Result.is_open_write end feature -- Package package_version_from_id_path_parameter (req: WSF_REQUEST; a_id_name: READABLE_STRING_GENERAL): detachable IRON_NODE_VERSION_PACKAGE require -- request_has_id_name_path_param: req.path_parameter (a_id_name) /= Void request_has_version_path_param: req.path_parameter ("version") /= Void do if attached {WSF_STRING} req.path_parameter (a_id_name) as s_id and then attached iron.database.version_package (iron_version (req), s_id.value) as l_version_package then Result := l_version_package end end package_from_id_path_parameter (req: WSF_REQUEST; a_id_name: READABLE_STRING_GENERAL): detachable IRON_NODE_PACKAGE require request_has_id_name_path_param: req.path_parameter (a_id_name) /= Void do if attached {WSF_STRING} req.path_parameter (a_id_name) as s_id and then attached iron.database.package (s_id.value) as l_package then Result := l_package end end feature -- Package form new_package_edit_form (p: detachable IRON_NODE_VERSION_PACKAGE; req: WSF_REQUEST; validating: BOOLEAN): WSF_FORM local f: WSF_FORM f_id: WSF_FORM_HIDDEN_INPUT f_name: WSF_FORM_TEXT_INPUT f_title: WSF_FORM_TEXT_INPUT f_desc: WSF_FORM_TEXTAREA f_package_file: WSF_FORM_TEXTAREA f_archive: WSF_FORM_FILE_INPUT f_archive_url: WSF_FORM_TEXT_INPUT f_submit: WSF_FORM_SUBMIT_INPUT f_fieldset: WSF_FORM_FIELD_SET do if p /= Void then create f.make (req.script_url (iron.package_version_update_resource (p)), "edit_package") if validating then create f_id.make ("id") else create f_id.make_with_text ("id", p.id.to_string_32) end f.extend (f_id) else create f.make (req.script_url (iron.package_version_create_resource (iron_version (req))), "create_package") end f.set_multipart_form_data_encoding_type create f_name.make ("name") f_name.set_label ("Name") f.extend (f_name) create f_title.make ("title") f_title.set_label ("Title") f_title.set_description ("Optional title, if unset, use `name' in user interfaces") f.extend (f_title) create f_desc.make ("description") f_desc.set_label ("Description") f.extend (f_desc) create f_package_file.make ("package_info") f_package_file.set_label ("Package Info") f.extend (f_package_file) create f_fieldset.make f_fieldset.set_legend ("Associated Archive") create f_archive.make ("archive") f_archive.set_label ("Upload archive file") f_fieldset.extend (f_archive) create f_archive_url.make ("archive-url") f_archive_url.set_label ("Get archive from url") f_archive_url.set_description ("If you have trouble uploading the archive file, the server can download from a public URL.") f_fieldset.extend (f_archive_url) -- if p /= Void and then p.has_archive then ---- f_fieldset.insert_after (create {WSF_FORM_RAW_TEXT}.make ("Has already an archive (" + p.archive_file_size.out + " octets)"), f_archive) -- f_fieldset.extend_text ("Has already an archive (" + p.archive_file_size.out + " octets)") -- end f.extend (f_fieldset) -- f.extend (f_file) if p /= Void then create f_submit.make_with_text ("op", "Update") else create f_submit.make_with_text ("op", "Create") end f.extend (f_submit) if validating then f_name.set_validation_action (agent (fd: WSF_FORM_DATA) do if attached {WSF_STRING} fd.item ("name") as if_name and then if_name.value.count >= 1 then -- Non empty string is ok else fd.report_invalid_field ("name", "Package name should contain at least 1 characters!") end end ) elseif p /= Void then if attached p.name as l_name then f_name.set_text_value (l_name) end if attached p.title as l_title then f_title.set_text_value (l_title) end if attached p.description as l_description then f_desc.set_text_value (l_description) end end -- if p /= Void and then p.has_archive then -- f.insert_after (create {WSF_FORM_RAW_TEXT}.make ("Has already an archive (" + p.archive_file_size.out + " octets)"), f_file) -- end Result := f end on_package_edit_form_processed (fd: WSF_FORM_DATA; req: WSF_REQUEST; res: WSF_RESPONSE) local m: like new_response_message s,t: STRING k: READABLE_STRING_GENERAL p: IRON_NODE_PACKAGE l_path_id: detachable READABLE_STRING_32 cl_path: CELL [detachable PATH] pv: detachable IRON_NODE_VERSION_PACKAGE l_name: detachable READABLE_STRING_32 pif: detachable IRON_PACKAGE_INFO_FILE do m := new_response_message (req) create s.make_empty if fd.has_error or not fd.is_valid then if attached fd.errors as errs then across errs as e loop t := "[Error] " if attached e.message as err_msg then m.add_error_message (err_msg) end end end res.send (m) else fd.apply_to_associated_form if attached {WSF_STRING} req.path_parameter ("id") as p_id then l_path_id := p_id.value end l_name := fd.string_item ("name") if attached fd.string_item ("package_info") as l_package_info_text then pif := iron.package_info_from_text (l_package_info_text) if l_name = Void and pif /= Void then l_name := pif.package_name end else pif := Void end if attached fd.string_item ("id") as l_id then if l_path_id /= Void then if l_id.is_case_insensitive_equal (l_path_id) then if attached iron.database.version_package (iron_version (req), l_id) as l_package then p := l_package.package else -- Error debug fd.report_error ("Package ["+ l_id +"] not found") end create p.make (l_id) end else fd.report_error ("Package id mismatch! " + l_path_id.out + " and " + l_id.out) create p.make (l_id) end else fd.report_error ("Package id is missing from URI!") create p.make (l_id) end elseif l_name /= Void then check no_id_item: fd.string_item ("id") = Void end if attached iron.database.package_by_name (l_name) as l_package then p := l_package if l_path_id /= Void then if not l_package.id.is_case_insensitive_equal (l_path_id) then fd.report_error ("Package id mismatch! " + l_path_id.out + " and " + l_package.id.out) end end elseif l_path_id /= Void then if attached iron.database.package (l_path_id) as l_package then p := l_package else -- Error fd.report_error ("Package ["+ l_path_id +"] not found") create p.make_empty p.set_name (l_name) end else -- Error -- fd.report_error ("Package named ["+ url_encoder.encoded_string (l_name) +"] not found, and id " + l_path_id + " is precised!") -- FIXME: check encoding for API create p.make_empty p.set_name (l_name) end elseif l_path_id /= Void then fd.report_error ("Missing package id from post!") create p.make (l_path_id) else create p.make_empty end if attached current_user (req) as l_user then if p.owner = Void then p.set_owner (l_user) elseif not user_has_permission_to_modify_package (l_user, p) then fd.report_error ("Only owner and administrator can modify current package.") end else fd.report_error ("Operation restricted to allowed user.") end if not fd.has_error then l_name := fd.string_item ("name") if l_name = Void and pif /= Void then l_name := pif.package_name end if l_name /= Void then p.set_name (l_name) end if attached fd.string_item ("title") as l_title then p.set_title (l_title) elseif pif /= Void then p.set_title (pif.title) end if attached fd.string_item ("description") as l_description then p.set_description (l_description) elseif pif /= Void then p.set_description (pif.description) end if pif /= Void then if attached pif.tags as l_tags then across l_tags as ic loop p.add_tag (ic) end end -- Links across pif.notes as ic loop k := @ ic.key if k.is_case_insensitive_equal ("title") or k.is_case_insensitive_equal ("description") or k.is_case_insensitive_equal ("tags") then -- Already handled elseif k.starts_with ("link[") and then ic.is_valid_as_string_8 then p.add_link (k.substring (6, k.count - 1), create {IRON_NODE_LINK}.make (ic.to_string_8, Void)) elseif k.starts_with ("links[") and then ic.is_valid_as_string_8 then p.add_link (k.substring (7, k.count - 1), create {IRON_NODE_LINK}.make (ic.to_string_8, Void)) else p.put (ic, k) end end end end if has_permission_to_modify_package (req, p) then if fd.has_error then if attached fd.errors as errs then across errs as e loop t := "[Error] " if attached e.message as err_msg then m.add_error_message (err_msg) end end end m.set_status_code ({HTTP_STATUS_CODE}.expectation_failed) else if p.has_id then iron.database.update_package (p) m.add_normal_message ("Package updated [" + p.id + "]") else iron.database.update_package (p) m.add_normal_message ("Package created [" + p.id + "]") end pv := database.version_package (iron_version (req), p.id) if pv = Void then create pv.make (p, iron_version (req)) m.add_normal_message ("Package version created [" + p.id + "] v:" + pv.version.value) iron.database.update_version_package (pv) end if pv /= Void then if attached {WSF_UPLOADED_FILE} fd.item ("archive") as l_file then iron.database.save_uploaded_package_archive (pv, l_file) elseif attached {WSF_STRING} fd.item ("archive-url") as l_archive_url and then not l_archive_url.is_empty then create cl_path.put (Void) download (l_archive_url.url_encoded_value, cl_path, req) if attached cl_path.item as l_downloaded_path then iron.database.save_package_archive (pv, l_downloaded_path, False) end end end end if pv /= Void then m.set_location (req.absolute_script_url (iron.package_version_view_resource (pv))) else m.set_location (req.absolute_script_url (iron.package_view_resource (p))) end res.send (m) else m := new_not_permitted_response_message (req) res.send (m) end end end feature -- Factory new_response_message (req: WSF_REQUEST): IRON_NODE_API_RESPONSE do create Result.make (req, iron) Result.set_iron_version (iron_version (req)) end new_not_permitted_response_message (req: WSF_REQUEST): IRON_NODE_API_RESPONSE do create Result.make_not_permitted (req, iron) Result.set_iron_version (iron_version (req)) end new_not_found_response_message (req: WSF_REQUEST): IRON_NODE_API_RESPONSE do create Result.make_not_found (req, iron) Result.set_iron_version (iron_version (req)) end feature {NONE} -- Implementation url_encoder: URL_ENCODER once create Result end note copyright: "Copyright (c) 1984-2021, Eiffel Software" license: "GPL version 2 (see http://www.eiffel.com/licensing/gpl.txt)" licensing_options: "http://www.eiffel.com/licensing" copying: "[ This file is part of Eiffel Software's Eiffel Development Environment. Eiffel Software's Eiffel Development Environment is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2 of the License (available at the URL listed under "license" above). Eiffel Software's Eiffel Development Environment is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Eiffel Software's Eiffel Development Environment; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ]" source: "[ Eiffel Software 5949 Hollister Ave., Goleta, CA 93117 USA Telephone 805-685-1006, Fax 805-685-6869 Website http://www.eiffel.com Customer support http://support.eiffel.com ]" end