-
Notifications
You must be signed in to change notification settings - Fork 3.2k
[#71630] POC: leverage friendly_id :history for persistent project identifier URIs #22295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7d7eb85
3fec880
01cedde
7c7aa05
a40e47e
4d3eaec
7ca47c5
d07f13f
db8ade3
054b7bb
0f24b9f
0aecd0d
39c6ff7
4ca48e0
abc9aae
a1f88c6
1713db5
51d5ec6
c42a6d5
50b2fdf
ba09157
391e9ec
b69328b
f4d42dc
c9fb8f8
e6e67d8
8413818
bf4fe67
c9da7de
3a8c642
44f00d8
babdc34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program 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; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program 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 this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| class CreateFriendlyIdSlugs < ActiveRecord::Migration[8.1] | ||
| def change | ||
| create_table :friendly_id_slugs do |t| | ||
| t.string :slug, null: false | ||
| t.bigint :sluggable_id, null: false | ||
| t.string :sluggable_type, limit: 50 | ||
| t.string :scope | ||
| t.datetime :created_at | ||
| end | ||
|
|
||
| add_index :friendly_id_slugs, %i[sluggable_type sluggable_id] | ||
| add_index :friendly_id_slugs, %i[slug sluggable_type], | ||
| length: { slug: 140, sluggable_type: 50 } | ||
| add_index :friendly_id_slugs, %i[slug sluggable_type scope], | ||
| length: { slug: 70, sluggable_type: 50, scope: 70 }, | ||
| unique: true | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program 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; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program 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 this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| class InitializeHistoricIdentifiers < ActiveRecord::Migration[8.1] | ||
| def up | ||
| execute <<~SQL.squish | ||
| INSERT INTO friendly_id_slugs (slug, sluggable_id, sluggable_type, scope, created_at) | ||
| SELECT p.identifier, p.id, 'Project', NULL, NOW() | ||
| FROM projects p | ||
| WHERE p.identifier IS NOT NULL | ||
| AND NOT EXISTS ( | ||
| SELECT 1 FROM friendly_id_slugs fis | ||
| WHERE fis.slug = p.identifier | ||
| AND fis.sluggable_id = p.id | ||
| AND fis.sluggable_type = 'Project' | ||
| AND fis.scope IS NULL | ||
| ) | ||
| SQL | ||
| end | ||
|
|
||
| def down | ||
| # nothing to do / not possible | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program 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; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program 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 this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module API | ||
| module Helpers | ||
| module HistoricalIdentifierRedirect | ||
| # Redirects API requests using a historical project identifier to the canonical URL | ||
| # with the project's current identifier. | ||
| # | ||
| # Returns a 301 Moved Permanently response when the request uses a historical | ||
| # (retired) project identifier. This ensures API responses always use canonical URLs. | ||
| # | ||
| # @param identifier_param [Symbol] The route parameter name (e.g., :id, :project, :of) | ||
| # @param project [Project] The loaded project instance | ||
| # | ||
| # @example In a Grape API endpoint | ||
| # route_param :id do | ||
| # after_validation do | ||
| # helpers ::API::Helpers::HistoricalIdentifierRedirect | ||
| # @project = Project.find(params[:id]) | ||
| # redirect_if_historical_identifier(:id, @project) | ||
| # end | ||
| # end | ||
| def redirect_if_historical_identifier(identifier_param, project) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @judithroth This helper is currently rather raw because Grape doesn't have the same URL construction capabilities as Rails, right? I might have some good news -- it looks like we have a helper that allows us to construct URLs neatly within Grape as well. 🙂 => quick example here Lemme know what you think -- at the very least, it looks like it's making tests pass.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that solution very much - thanks for suggesting it! |
||
| param_value = params[identifier_param] | ||
| return unless request.get? && param_value.friendly_id? && param_value != project.identifier | ||
|
|
||
| redirect canonical_identifier_path(identifier_param, param_value, project), permanent: true | ||
| end | ||
|
|
||
|
|
||
| def canonical_identifier_path(identifier_param, param_value, project) | ||
| # Replace the old identifier in the path. | ||
| # This prevents Host header injection and open redirect attacks. | ||
| new_path = request.path.sub( | ||
| %r{(/)#{Regexp.escape(param_value)}(/|-|$)}, | ||
| "\\1#{project.identifier}\\2" | ||
| ) | ||
|
|
||
| # Replace the old identifier in query parameters if present | ||
| if request.query_string.present? | ||
| new_query_string = request.query_string.gsub( | ||
| /(\A|&)#{Regexp.escape(identifier_param.to_s)}=#{Regexp.escape(param_value)}(&|\z)/, | ||
| "\\1#{identifier_param}=#{CGI.escape(project.identifier)}\\2" | ||
| ) | ||
| new_path += "?#{new_query_string}" | ||
| end | ||
|
|
||
| new_path | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,17 +33,26 @@ module V3 | |
| module Projects | ||
| class AvailableParentsAPI < ::API::OpenProjectAPI | ||
| resource :available_parent_projects do | ||
| helpers ::API::Helpers::HistoricalIdentifierRedirect | ||
|
|
||
| after_validation do | ||
| authorize_globally(:add_project) do | ||
| authorize_in_any_project(%i[add_subprojects edit_project]) | ||
| end | ||
|
|
||
| # Handle redirect for historical identifier in :of query param | ||
| if params[:of] | ||
| @of_project = Project.find(params[:of]) | ||
| redirect_if_historical_identifier(:of, @of_project) | ||
| end | ||
|
Comment on lines
38
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should look into this, probably not using |
||
| end | ||
|
|
||
| get &::API::V3::Utilities::Endpoints::SqlFallbackedIndex.new( | ||
| model: Project, | ||
| scope: -> do | ||
| project = if params[:of] | ||
| Project.find(params[:of]) | ||
| # Reuse @of_project if available (already found in after_validation) | ||
| @of_project || Project.find(params[:of]) | ||
| else | ||
| Project.new(workspace_type: params[:workspace_type] || "project") | ||
| end | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.