Commit 9a785ebf authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2018-05-18' into 'master'

CE upstream - 2018-05-18 12:27 UTC

Closes gitlab-org/distribution/team-tasks#87 et gitlab-qa#245

See merge request gitlab-org/gitlab-ee!5763
parents f253d1de 63b345a9
...@@ -169,7 +169,7 @@ hits. They are not always necessary, but very convenient. ...@@ -169,7 +169,7 @@ hits. They are not always necessary, but very convenient.
If you are an expert in a particular area, it makes it easier to find issues to If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an work on. You can also subscribe to those labels to receive an email each time an
issue is labelled with a subject label corresponding to your expertise. issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api, Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry". ~issues, ~"merge requests", ~labels, and ~"container registry".
...@@ -297,7 +297,24 @@ any potential community contributor to @-mention per above. ...@@ -297,7 +297,24 @@ any potential community contributor to @-mention per above.
## Implement design & UI elements ## Implement design & UI elements
Please see the [UX Guide for GitLab]. For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
The UX team uses labels to manage their workflow.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook.
Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue.
The UX team has a special type label called ~"design artifact". This label indicates that the final output
for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the
existing issue or decide to create a whole new issue for the purpose of development.
## Issue tracker ## Issue tracker
......
...@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts { ...@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super(); super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
...@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts { ...@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project'); this.enabledHelp.push('.hidden-shortcut.project');
......
...@@ -306,8 +306,18 @@ ...@@ -306,8 +306,18 @@
} }
.preview-container { .preview-container {
flex-grow: 1;
position: relative;
.md-previewer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
padding: $gl-padding;
}
.file-container { .file-container {
background-color: $gray-darker; background-color: $gray-darker;
...@@ -347,10 +357,6 @@ ...@@ -347,10 +357,6 @@
color: $diff-image-info-color; color: $diff-image-info-color;
} }
} }
.md-previewer {
padding: $gl-padding;
}
} }
.ide-mode-tabs { .ide-mode-tabs {
......
...@@ -188,7 +188,7 @@ module Ci ...@@ -188,7 +188,7 @@ module Ci
end end
def playable? def playable?
action? && (manual? || complete?) action? && (manual? || retryable?)
end end
def action? def action?
......
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
.key e .key v
%td %td
Go to the project's activity feed Go to the project's activity feed
%tr %tr
...@@ -172,6 +172,18 @@ ...@@ -172,6 +172,18 @@
.key m .key m
%td %td
Go to merge requests Go to merge requests
%tr
%td.shortcut
.key g
.key e
%td
Go to environments
%tr
%td.shortcut
.key g
.key k
%td
Go to kubernetes
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
......
...@@ -221,7 +221,7 @@ ...@@ -221,7 +221,7 @@
- if project_nav_tab? :clusters - if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project) - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do = nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span %span
= _('Kubernetes') = _('Kubernetes')
- if show_cluster_hint - if show_cluster_hint
......
---
title: Add API endpoint to render markdown text
merge_request: 18926
author: "@blackst0ne"
type: added
---
title: Fix unscrollable Markdown preview of WebIDE on Firefox
merge_request:
author:
type: fixed
---
title: Adds keyboard shortcut `g e` for Environments on Project pages
merge_request: 19002
author:
type: added
---
title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
merge_request: 19002
author:
type: added
---
title: Changes keyboard shortcut of Activity feed to `g v`
merge_request: 19002
author:
type: changed
---
title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
merge_request: 19002
author:
type: removed
---
title: Add support for variables expression pattern matching syntax
merge_request: 18902
author:
type: added
---
title: Do not allow to trigger manual actions that were skipped
merge_request: 18985
author:
type: fixed
---
title: Fix api_json.log not always reporting the right HTTP status code
merge_request:
author:
type: fixed
...@@ -37,6 +37,7 @@ following locations: ...@@ -37,6 +37,7 @@ following locations:
- [Keys](keys.md) - [Keys](keys.md)
- [Labels](labels.md) - [Labels](labels.md)
- [License](license.md) - [License](license.md)
- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md) - [Merge Requests](merge_requests.md)
- [Merge Request Approvals](merge_request_approvals.md) **[STARTER]** - [Merge Request Approvals](merge_request_approvals.md) **[STARTER]**
- [Project milestones](milestones.md) - [Project milestones](milestones.md)
......
# Markdown API
> [Introduced][ce-18926] in GitLab 11.0.
Available only in APIv4.
## Render an arbitrary Markdown document
```
POST /api/v4/markdown
```
| Attribute | Type | Required | Description |
| --------- | ------- | ------------- | ------------------------------------------ |
| `text` | string | yes | The markdown text to render |
| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` |
| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. |
```bash
curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown
```
Response example:
```json
{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" }
```
[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926
...@@ -551,6 +551,16 @@ Below you can find supported syntax reference: ...@@ -551,6 +551,16 @@ Below you can find supported syntax reference:
`$STAGING` value needs to a string, with length higher than zero. `$STAGING` value needs to a string, with length higher than zero.
Variable that contains only whitespace characters is not an empty variable. Variable that contains only whitespace characters is not an empty variable.
1. Pattern matching _(added in 11.0)_
> Example: `$VARIABLE =~ /^content.*/`
It is possible perform pattern matching against a variable and regular
expression. Expression like this evaluates to truth if matches are found.
Pattern matching is case-sensitive by default. Use `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive.
### Unsupported predefined variables ### Unsupported predefined variables
Because GitLab evaluates variables before creating jobs, we do not support a Because GitLab evaluates variables before creating jobs, we do not support a
......
...@@ -344,10 +344,11 @@ job: ...@@ -344,10 +344,11 @@ job:
kubernetes: active kubernetes: active
``` ```
Example of using variables expressions: Examples of using variables expressions:
```yaml ```yaml
deploy: deploy:
script: cap staging deploy
only: only:
refs: refs:
- branches - branches
...@@ -356,6 +357,16 @@ deploy: ...@@ -356,6 +357,16 @@ deploy:
- $STAGING - $STAGING
``` ```
Another use case is exluding jobs depending on a commit message _(added in 11.0)_:
```yaml
end-to-end:
script: rake test:end-to-end
except:
variables:
- $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
```
Learn more about variables expressions on [a separate page][variables-expressions]. Learn more about variables expressions on [a separate page][variables-expressions].
## `tags` ## `tags`
......
...@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so ...@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users. alternative authentication methods to your users.
### Remove Jenkins CI Service entries from the database ### Remove Service Integration entries from the database
The `JenkinsService` class is only available on the Enterprise Edition codebase, The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase,
so if you downgrade to the Community Edition, you'll come across the following so if you downgrade to the Community Edition, you'll come across the following
error: error:
...@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o ...@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o
use another column for that information.) use another column for that information.)
``` ```
or
```
Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This
error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
use another column for that information.)
```
All services are created automatically for every project you have, so in order All services are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all instances of the to avoid getting this error, you need to remove all instances of the
`JenkinsService` from your database: `JenkinsService` and `GithubService` from your database:
**Omnibus Installation** **Omnibus Installation**
``` ```
$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" $ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
``` ```
**Source Installation** **Source Installation**
``` ```
$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production $ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
``` ```
### Secret variables environment scopes ### Secret variables environment scopes
......
...@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from ...@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-rake gitlab:backup:create docker exec -t <container name> gitlab-rake gitlab:backup:create
``` ```
If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can
run the backup task on the gitlab application pod using kubectl
```
kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
```
Example output: Example output:
``` ```
...@@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta ...@@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta
version of GitLab, the restore command will abort with an error. Install the version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again. [correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
### Restore for Docker image and gitlab-omnibus helm chart
For GitLab installations using docker image or the gitlab-omnibus helm chart on
a Kubernetes cluster, restore task expects the restore directories to be empty.
However, with docker and Kubernetes volume mounts, some system level directories
may be created at the volume roots, like `lost+found` directory found in Linux
operating systems. These directories are usually owned by `root`, which can
cause access permission errors since the restore rake task runs as `git` user.
So, to restore a GitLab installation, users have to confirm the restore target
directories are empty.
For both these installation types, the backup tarball has to be available in the
backup location (default location is `/var/opt/gitlab/backups`).
For docker installations, the restore task can be run from host using the
command
```
docker exec -it <name of container> gitlab-rake gitlab:backup:restore
```
Similarly, for gitlab-omnibus helm chart, the restore task can be run on the
gitlab application pod using kubectl
```
kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore
```
## Alternative backup strategies ## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
......
...@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' ...@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description | | Keyboard Shortcut | Description |
| ----------------- | ----------- | | ----------------- | ----------- |
| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | | <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed | | <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>f</kbd> | Go to files | | <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | | <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs | | <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph | | <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts | | <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues | | <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests | | <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets | | <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
| <kbd>t</kbd> | Go to finding file | | <kbd>t</kbd> | Go to finding file |
| <kbd>i</kbd> | New issue | | <kbd>i</kbd> | New issue |
......
...@@ -8,7 +8,8 @@ module API ...@@ -8,7 +8,8 @@ module API
PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
use GrapeLogging::Middleware::RequestLogger, insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
logger: Logger.new(LOG_FILENAME), logger: Logger.new(LOG_FILENAME),
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [ include: [
...@@ -151,6 +152,7 @@ module API ...@@ -151,6 +152,7 @@ module API
mount ::API::Keys mount ::API::Keys
mount ::API::Labels mount ::API::Labels
mount ::API::Lint mount ::API::Lint
mount ::API::Markdown
mount ::API::Members mount ::API::Members
mount ::API::MergeRequestApprovals mount ::API::MergeRequestApprovals
mount ::API::MergeRequestDiffs mount ::API::MergeRequestDiffs
......
module API
class Markdown < Grape::API
params do
requires :text, type: String, desc: "The markdown text to render"
optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown"
optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown"
end
resource :markdown do
desc "Render markdown text" do
detail "This feature was introduced in GitLab 11.0."
end
post do
# Explicitly set CommonMark as markdown engine to use.
# Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done.
context = { markdown_engine: :common_mark, only_path: false }
if params[:project]
project = Project.find_by_full_path(params[:project])
not_found!("Project") unless can?(current_user, :read_project, project)
context[:project] = project
else
context[:skip_project_check] = true
end
context[:pipeline] = params[:gfm] ? :full : :plain_markdown
{ html: Banzai.render(params[:text], context) }
end
end
end
end
...@@ -73,7 +73,7 @@ module Banzai ...@@ -73,7 +73,7 @@ module Banzai
# #
# Note that while the key might exist, its value could be nil! # Note that while the key might exist, its value could be nil!
def validate def validate
needs :project needs :project unless skip_project_check?
end end
# Iterates over all <a> and text() nodes in a document. # Iterates over all <a> and text() nodes in a document.
......
...@@ -43,9 +43,9 @@ module Banzai ...@@ -43,9 +43,9 @@ module Banzai
end end
def self.transform_context(context) def self.transform_context(context)
context.merge( context[:only_path] = true unless context.key?(:only_path)
only_path: true,
context.merge(
# EmojiFilter # EmojiFilter
asset_host: Gitlab::Application.config.asset_host, asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url asset_root: Gitlab.config.gitlab.base_url
......
module Gitlab
module Ci
module Pipeline
module Expression
ExpressionError = Class.new(StandardError)
RuntimeError = Class.new(ExpressionError)
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Matches < Lexeme::Operator
PATTERN = /=~/.freeze
def initialize(left, right)
@left = left
@right = right
end
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
regexp.scan(text.to_s).any?
end
def self.build(_value, behind, ahead)
new(behind, ahead)
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
require_dependency 're2'
class Pattern < Lexeme::Value
PATTERN = %r{^/.+/[ismU]*$}.freeze
def initialize(regexp)
@value = regexp
unless Gitlab::UntrustedRegexp.valid?(@value)
raise Lexer::SyntaxError, 'Invalid regular expression!'
end
end
def evaluate(variables = {})
Gitlab::UntrustedRegexp.fabricate(@value)
rescue RegexpError
raise Expression::RuntimeError, 'Invalid regular expression!'
end
def self.build(string)
new(string)
end
end
end
end
end
end
end
...@@ -5,15 +5,17 @@ module Gitlab ...@@ -5,15 +5,17 @@ module Gitlab
class Lexer class Lexer
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
SyntaxError = Class.new(Expression::ExpressionError)
LEXEMES = [ LEXEMES = [
Expression::Lexeme::Variable, Expression::Lexeme::Variable,
Expression::Lexeme::String, Expression::Lexeme::String,
Expression::Lexeme::Pattern,
Expression::Lexeme::Null, Expression::Lexeme::Null,
Expression::Lexeme::Equals Expression::Lexeme::Equals,
Expression::Lexeme::Matches
].freeze ].freeze
SyntaxError = Class.new(Statement::StatementError)
MAX_TOKENS = 100 MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS) def initialize(statement, max_tokens: MAX_TOKENS)
......
...@@ -3,15 +3,16 @@ module Gitlab ...@@ -3,15 +3,16 @@ module Gitlab
module Pipeline module Pipeline
module Expression module Expression
class Statement class Statement
StatementError = Class.new(StandardError) StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [ GRAMMAR = [
%w[variable],
%w[variable equals string], %w[variable equals string],
%w[variable equals variable], %w[variable equals variable],
%w[variable equals null], %w[variable equals null],
%w[string equals variable], %w[string equals variable],
%w[null equals variable], %w[null equals variable],
%w[variable] %w[variable matches pattern]
].freeze ].freeze
def initialize(statement, variables = {}) def initialize(statement, variables = {})
...@@ -35,11 +36,13 @@ module Gitlab ...@@ -35,11 +36,13 @@ module Gitlab
def truthful? def truthful?
evaluate.present? evaluate.present?
rescue Expression::ExpressionError
false
end end
def valid? def valid?
parse_tree.is_a?(Lexeme::Base) parse_tree.is_a?(Lexeme::Base)
rescue StatementError rescue Expression::ExpressionError
false false
end end
end end
......
...@@ -9,7 +9,9 @@ module Gitlab ...@@ -9,7 +9,9 @@ module Gitlab
# there is a strict limit on total execution time. See the RE2 documentation # there is a strict limit on total execution time. See the RE2 documentation
# at https://github.com/google/re2/wiki/Syntax for more details. # at https://github.com/google/re2/wiki/Syntax for more details.
class UntrustedRegexp class UntrustedRegexp
delegate :===, to: :regexp require_dependency 're2'
delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false) def initialize(pattern, multiline: false)
if multiline if multiline
...@@ -35,6 +37,10 @@ module Gitlab ...@@ -35,6 +37,10 @@ module Gitlab
RE2.Replace(text, regexp, rewrite) RE2.Replace(text, regexp, rewrite)
end end
def ==(other)
self.source == other.source
end
# Handles regular expressions with the preferred RE2 library where possible # Handles regular expressions with the preferred RE2 library where possible
# via UntustedRegex. Falls back to Ruby's built-in regular expression library # via UntustedRegex. Falls back to Ruby's built-in regular expression library
# when the syntax would be invalid in RE2. # when the syntax would be invalid in RE2.
...@@ -48,6 +54,24 @@ module Gitlab ...@@ -48,6 +54,24 @@ module Gitlab
Regexp.new(pattern) Regexp.new(pattern)
end end
def self.valid?(pattern)
!!self.fabricate(pattern)
rescue RegexpError
false
end
def self.fabricate(pattern)
matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$})
raise RegexpError, 'Invalid regular expression!' if matches.nil?
expression = matches[:regexp]
flags = matches[:flags]
expression.prepend("(?#{flags})") if flags.present?
self.new(expression, multiline: false)
end
private private
attr_reader :regexp attr_reader :regexp
......
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
expect(page).to have_content('This is a merge request') expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature') expect(page).to have_content('Great feature')
expect(page).to have_content('Opened less than a minute ago') expect(page).to have_content(/Opened [\w\s]+ a minute ago/)
end end
end end
end end
...@@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do ...@@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do
context 'when navigating to the Project pages' do context 'when navigating to the Project pages' do
it 'redirects to the details page' do it 'redirects to the details page' do
visit project_issues_path(project)
find('body').native.send_key('g') find('body').native.send_key('g')
find('body').native.send_key('p') find('body').native.send_key('p')
...@@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do ...@@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do
it 'redirects to the activity page' do it 'redirects to the activity page' do
find('body').native.send_key('g') find('body').native.send_key('g')
find('body').native.send_key('e') find('body').native.send_key('v')
expect(page).to have_active_navigation('Project') expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Activity') expect(page).to have_active_sub_navigation('Activity')
...@@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do ...@@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do
expect(page).to have_active_sub_navigation('List') expect(page).to have_active_sub_navigation('List')
end end
it 'redirects to the issue board page' do
find('body').native.send_key('g')
find('body').native.send_key('b')
expect(page).to have_active_navigation('Issues')
expect(page).to have_active_sub_navigation('Board')
end
it 'redirects to the new issue page' do it 'redirects to the new issue page' do
find('body').native.send_key('i') find('body').native.send_key('i')
expect(page).to have_content(project.title) expect(page).to have_content(project.title)
expect(page).to have_content('New Issue')
end end
end end
...@@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do ...@@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do
end end
end end
context 'when navigating to the CI / CD pages' do
it 'redirects to the Jobs page' do
find('body').native.send_key('g')
find('body').native.send_key('j')
expect(page).to have_active_navigation('CI / CD')
expect(page).to have_active_sub_navigation('Jobs')
end
end
context 'when navigating to the Operations pages' do
it 'redirects to the Environments page' do
find('body').native.send_key('g')
find('body').native.send_key('e')
expect(page).to have_active_navigation('Operations')
expect(page).to have_active_sub_navigation('Environments')
end
it 'redirects to the Kubernetes page' do
find('body').native.send_key('g')
find('body').native.send_key('k')
expect(page).to have_active_navigation('Operations')
expect(page).to have_active_sub_navigation('Kubernetes')
end
end
context 'when navigating to the Snippets pages' do context 'when navigating to the Snippets pages' do
it 'redirects to the snippets page' do it 'redirects to the snippets page' do
find('body').native.send_key('g') find('body').native.send_key('g')
......
...@@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do ...@@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do
context 'when specifying invalid variables expressions token' do context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } } let(:config) { { variables: ['$MY_VAR == 123'] } }
it 'reports an error about invalid statement' do it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when using invalid variables expressions regexp' do
let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/ expect(entry.errors).to include /invalid expression syntax/
end end
end end
......
require 'fast_spec_helper'
require_dependency 're2'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
let(:left) { double('left') }
let(:right) { double('right') }
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('=~', left, right))
.to be_a(described_class)
end
end
describe '.type' do
it 'is an operator' do
expect(described_class.type).to eq :operator
end
end
describe '#evaluate' do
it 'returns false when left and right do not match' do
allow(left).to receive(:evaluate).and_return('my-string')
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('something'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq false
end
it 'returns true when left and right match' do
allow(left).to receive(:evaluate).and_return('my-awesome-string')
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq true
end
it 'supports matching against a nil value' do
allow(left).to receive(:evaluate).and_return(nil)
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('pattern'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq false
end
it 'supports multiline strings' do
allow(left).to receive(:evaluate).and_return <<~TEXT
My awesome contents
My-text-string!
TEXT
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('text-string'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq true
end
it 'supports regexp flags' do
allow(left).to receive(:evaluate).and_return <<~TEXT
My AWESOME content
TEXT
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq true
end
end
end
require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('/.*/'))
.to be_a(described_class)
end
it 'raises an error if pattern is invalid' do
expect { described_class.build('/ some ( thin/i') }
.to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError)
end
end
describe '.type' do
it 'is a value lexeme' do
expect(described_class.type).to eq :value
end
end
describe '.scan' do
it 'correctly identifies a pattern token' do
scanner = StringScanner.new('/pattern/')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('pattern')
end
it 'is a greedy scanner for regexp boundaries' do
scanner = StringScanner.new('/some .* / pattern/')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
end
it 'does not allow to use an empty pattern' do
scanner = StringScanner.new(%(//))
token = described_class.scan(scanner)
expect(token).to be_nil
end
it 'support single flag' do
scanner = StringScanner.new('/pattern/i')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('(?i)pattern')
end
it 'support multiple flags' do
scanner = StringScanner.new('/pattern/im')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
end
it 'does not support arbitrary flags' do
scanner = StringScanner.new('/pattern/x')
token = described_class.scan(scanner)
expect(token).to be_nil
end
end
describe '#evaluate' do
it 'returns a regular expression' do
regexp = described_class.new('/abc/')
expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc')
end
it 'raises error if evaluated regexp is not valid' do
allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true)
regexp = described_class.new('/invalid ( .*/')
expect { regexp.evaluate }
.to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError)
end
end
end
...@@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do ...@@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
end end
describe '#tokens' do describe '#tokens' do
it 'tokenss single value' do it 'returns single value' do
tokens = described_class.new('$VARIABLE').tokens tokens = described_class.new('$VARIABLE').tokens
expect(tokens).to be_one expect(tokens).to be_one
...@@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do ...@@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens).to all(be_an_instance_of(token_class)) expect(tokens).to all(be_an_instance_of(token_class))
end end
it 'tokenss multiple values of the same token' do it 'returns multiple values of the same token' do
tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
expect(tokens.size).to eq 2 expect(tokens.size).to eq 2
expect(tokens).to all(be_an_instance_of(token_class)) expect(tokens).to all(be_an_instance_of(token_class))
end end
it 'tokenss multiple values with different tokens' do it 'returns multiple values with different tokens' do
tokens = described_class.new('$VARIABLE "text" "value"').tokens tokens = described_class.new('$VARIABLE "text" "value"').tokens
expect(tokens.size).to eq 3 expect(tokens.size).to eq 3
...@@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do ...@@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens.third.value).to eq '"value"' expect(tokens.third.value).to eq '"value"'
end end
it 'tokenss tokens and operators' do it 'returns tokens and operators' do
tokens = described_class.new('$VARIABLE == "text"').tokens tokens = described_class.new('$VARIABLE == "text"').tokens
expect(tokens.size).to eq 3 expect(tokens.size).to eq 3
......
require 'spec_helper' require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do describe '#tree' do
......
require 'spec_helper' require 'fast_spec_helper'
require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do describe Gitlab::Ci::Pipeline::Expression::Statement do
subject do subject do
...@@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'== "123"', # invalid left side '== "123"', # invalid left side
'"some string"', # only string provided '"some string"', # only string provided
'$VAR ==', # invalid right side '$VAR ==', # invalid right side
'12345', # unknown syntax 'null', # missing lexemes
'' # empty statement '' # empty statement
] ]
...@@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context "when expression grammar is #{syntax.inspect}" do context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax } let(:text) { syntax }
it 'aises a statement error exception' do it 'raises a statement error exception' do
expect { subject.parse_tree } expect { subject.parse_tree }
.to raise_error described_class::StatementError .to raise_error described_class::StatementError
end end
...@@ -82,49 +83,67 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do ...@@ -82,49 +83,67 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end end
describe '#evaluate' do describe '#evaluate' do
statements = [ using RSpec::Parameterized::TableSyntax
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'my variable'", true], where(:expression, :value) do
['"my variable" == $PRESENT_VARIABLE', true], '$PRESENT_VARIABLE == "my variable"' | true
['$PRESENT_VARIABLE == null', false], '"my variable" == $PRESENT_VARIABLE' | true
['$EMPTY_VARIABLE == null', false], '$PRESENT_VARIABLE == null' | false
['"" == $EMPTY_VARIABLE', true], '$EMPTY_VARIABLE == null' | false
['$EMPTY_VARIABLE', ''], '"" == $EMPTY_VARIABLE' | true
['$UNDEFINED_VARIABLE == null', true], '$EMPTY_VARIABLE' | ''
['null == $UNDEFINED_VARIABLE', true], '$UNDEFINED_VARIABLE == null' | true
['$PRESENT_VARIABLE', 'my variable'], 'null == $UNDEFINED_VARIABLE' | true
['$UNDEFINED_VARIABLE', nil] '$PRESENT_VARIABLE' | 'my variable'
] '$UNDEFINED_VARIABLE' | nil
"$PRESENT_VARIABLE =~ /var.*e$/" | true
statements.each do |expression, value| "$PRESENT_VARIABLE =~ /^var.*/" | false
context "when using expression `#{expression}`" do "$EMPTY_VARIABLE =~ /var.*/" | false
"$UNDEFINED_VARIABLE =~ /var.*/" | false
"$PRESENT_VARIABLE =~ /VAR.*/i" | true
end
with_them do
let(:text) { expression } let(:text) { expression }
it "evaluates to `#{value.inspect}`" do it "evaluates to `#{params[:value].inspect}`" do
expect(subject.evaluate).to eq value expect(subject.evaluate).to eq value
end end
end end
end end
end
describe '#truthful?' do describe '#truthful?' do
statements = [ using RSpec::Parameterized::TableSyntax
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'no match'", false], where(:expression, :value) do
['$UNDEFINED_VARIABLE == null', true], '$PRESENT_VARIABLE == "my variable"' | true
['$PRESENT_VARIABLE', true], "$PRESENT_VARIABLE == 'no match'" | false
['$UNDEFINED_VARIABLE', false], '$UNDEFINED_VARIABLE == null' | true
['$EMPTY_VARIABLE', false] '$PRESENT_VARIABLE' | true
] '$UNDEFINED_VARIABLE' | false
'$EMPTY_VARIABLE' | false
'$INVALID = 1' | false
"$PRESENT_VARIABLE =~ /var.*/" | true
"$UNDEFINED_VARIABLE =~ /var.*/" | false
end
statements.each do |expression, value| with_them do
context "when using expression `#{expression}`" do
let(:text) { expression } let(:text) { expression }
it "returns `#{value.inspect}`" do it "returns `#{params[:value].inspect}`" do
expect(subject.truthful?).to eq value expect(subject.truthful?).to eq value
end end
end end
context 'when evaluating expression raises an error' do
let(:text) { '$PRESENT_VARIABLE' }
it 'returns false' do
allow(subject).to receive(:evaluate)
.and_raise(described_class::StatementError)
expect(subject.truthful?).to be_falsey
end
end end
end end
end end
require 'spec_helper' require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Token do describe Gitlab::Ci::Pipeline::Expression::Token do
let(:value) { '$VARIABLE' } let(:value) { '$VARIABLE' }
......
require 'spec_helper' require 'fast_spec_helper'
require 'support/shared_examples/malicious_regexp_shared_examples'
describe Gitlab::UntrustedRegexp do describe Gitlab::UntrustedRegexp do
describe '.valid?' do
it 'returns true if regexp is valid' do
expect(described_class.valid?('/some ( thing/'))
.to be false
end
it 'returns true if regexp is invalid' do
expect(described_class.valid?('/some .* thing/'))
.to be true
end
end
describe '.fabricate' do
context 'when regexp is using /regexp/ scheme with flags' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate('/something/i')
expect(regexp).to eq described_class.new('(?i)something')
expect(regexp.scan('SOMETHING')).to be_one
end
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate('/something/im')
expect(regexp).to eq described_class.new('(?im)something')
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate('/something/')
expect(regexp).to eq described_class.new('something')
end
end
context 'when regexp is a raw pattern' do
it 'raises an error' do
expect { described_class.fabricate('some .* thing') }
.to raise_error(RegexpError)
end
end
end
describe '#initialize' do describe '#initialize' do
subject { described_class.new(pattern) } subject { described_class.new(pattern) }
......
...@@ -1289,6 +1289,46 @@ describe Ci::Build do ...@@ -1289,6 +1289,46 @@ describe Ci::Build do
end end
end end
describe '#playable?' do
context 'when build is a manual action' do
context 'when build has been skipped' do
subject { build_stubbed(:ci_build, :manual, status: :skipped) }
it { is_expected.not_to be_playable }
end
context 'when build has been canceled' do
subject { build_stubbed(:ci_build, :manual, status: :canceled) }
it { is_expected.to be_playable }
end
context 'when build is successful' do
subject { build_stubbed(:ci_build, :manual, status: :success) }
it { is_expected.to be_playable }
end
context 'when build has failed' do
subject { build_stubbed(:ci_build, :manual, status: :failed) }
it { is_expected.to be_playable }
end
context 'when build is a manual untriggered action' do
subject { build_stubbed(:ci_build, :manual, status: :manual) }
it { is_expected.to be_playable }
end
end
context 'when build is not a manual action' do
subject { build_stubbed(:ci_build, :success) }
it { is_expected.not_to be_playable }
end
end
describe 'project settings' do describe 'project settings' do
describe '#allow_git_fetch' do describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do it 'return project allow_git_fetch configuration' do
......
require "spec_helper"
describe API::Markdown do
RSpec::Matchers.define_negated_matcher :exclude, :include
describe "POST /markdown" do
let(:user) {} # No-op. It gets overwritten in the contexts below.
before do
post api("/markdown", user), params
end
shared_examples "rendered markdown text without GFM" do
it "renders markdown text" do
expect(response).to have_http_status(201)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["html"]).to eq("<p>#{text}</p>")
end
end
shared_examples "404 Project Not Found" do
it "responses with 404 Not Found" do
expect(response).to have_http_status(404)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["message"]).to eq("404 Project Not Found")
end
end
context "when arguments are invalid" do
context "when text is missing" do
let(:params) { {} }
it "responses with 400 Bad Request" do
expect(response).to have_http_status(400)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["error"]).to eq("text is missing")
end
end
context "when project is not found" do
let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } }
it_behaves_like "404 Project Not Found"
end
end
context "when arguments are valid" do
set(:project) { create(:project) }
set(:issue) { create(:issue, project: project) }
let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" }
context "when not using gfm" do
context "without project" do
let(:params) { { text: text } }
it_behaves_like "rendered markdown text without GFM"
end
context "with project" do
let(:params) { { text: text, project: project.full_path } }
context "when not authorized" do
it_behaves_like "404 Project Not Found"
end
context "when authorized" do
let(:user) { project.owner }
it_behaves_like "rendered markdown text without GFM"
end
end
end
context "when using gfm" do
context "without project" do
let(:params) { { text: text, gfm: true } }
it "renders markdown text" do
expect(response).to have_http_status(201)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!")
.and include('data-name="tada"')
.and include('data-name="100"')
.and include("#1")
.and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
.and exclude("#1</a>")
end
end
context "with project" do
let(:params) { { text: text, gfm: true, project: project.full_path } }
let(:user) { project.owner }
it "renders markdown text" do
expect(response).to have_http_status(201)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!")
.and include('data-name="tada"')
.and include('data-name="100"')
.and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
.and include("#1</a>")
end
end
end
end
end
end
require 'timeout'
shared_examples 'malicious regexp' do shared_examples 'malicious regexp' do
let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' } let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' } let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment