ronin 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/ChangeLog.md +23 -0
- data/README.md +19 -13
- data/Rakefile +2 -1
- data/gemspec.yml +18 -17
- data/lib/bond/completions/ronin.rb +147 -0
- data/lib/ronin/auto_load.rb +30 -28
- data/lib/ronin/database/migrations/1.0.0.rb +1 -0
- data/lib/ronin/model/has_authors.rb +92 -2
- data/lib/ronin/model/has_description.rb +54 -2
- data/lib/ronin/model/has_license.rb +101 -2
- data/lib/ronin/model/has_name.rb +72 -2
- data/lib/ronin/model/has_title.rb +52 -2
- data/lib/ronin/model/has_unique_name.rb +93 -2
- data/lib/ronin/model/has_version.rb +58 -2
- data/lib/ronin/model/model.rb +91 -52
- data/lib/ronin/os.rb +30 -15
- data/lib/ronin/repository.rb +1 -1
- data/lib/ronin/ronin.rb +0 -15
- data/lib/ronin/script/script.rb +257 -2
- data/lib/ronin/ui/console.rb +2 -199
- data/lib/ronin/ui/console/commands.rb +164 -0
- data/lib/ronin/ui/console/console.rb +215 -0
- data/lib/ronin/ui/console/context.rb +95 -0
- data/lib/ronin/version.rb +1 -1
- data/spec/os_spec.rb +18 -13
- metadata +206 -239
- data/lib/ronin/class_methods.rb +0 -49
- data/lib/ronin/model/class_methods.rb +0 -58
- data/lib/ronin/model/has_authors/class_methods.rb +0 -60
- data/lib/ronin/model/has_authors/has_authors.rb +0 -70
- data/lib/ronin/model/has_description/class_methods.rb +0 -49
- data/lib/ronin/model/has_description/has_description.rb +0 -49
- data/lib/ronin/model/has_license/class_methods.rb +0 -68
- data/lib/ronin/model/has_license/has_license.rb +0 -71
- data/lib/ronin/model/has_name/class_methods.rb +0 -48
- data/lib/ronin/model/has_name/has_name.rb +0 -62
- data/lib/ronin/model/has_title/class_methods.rb +0 -48
- data/lib/ronin/model/has_title/has_title.rb +0 -48
- data/lib/ronin/model/has_unique_name/class_methods.rb +0 -51
- data/lib/ronin/model/has_unique_name/has_unique_name.rb +0 -78
- data/lib/ronin/model/has_version/class_methods.rb +0 -54
- data/lib/ronin/model/has_version/has_version.rb +0 -48
- data/lib/ronin/script/class_methods.rb +0 -84
- data/lib/ronin/script/instance_methods.rb +0 -217
data/ChangeLog.md
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
### 1.2.0 / 2011-08-15
|
2
|
+
|
3
|
+
* Require dm-is-predefined ~> 0.4.
|
4
|
+
* Added {Ronin::UI::Console::Context}.
|
5
|
+
* Added custom tab-completion to {Ronin::UI::Console} for:
|
6
|
+
* {Ronin::IPAddress}
|
7
|
+
* {Ronin::HostName}
|
8
|
+
* {Ronin::EmailAddress}
|
9
|
+
* {Ronin::URL}
|
10
|
+
* Paths
|
11
|
+
* Commands
|
12
|
+
* Added the ability to run commands in Ronin Console, via the
|
13
|
+
`!command --args` syntax.
|
14
|
+
* Added custom `!command`s to the Ronin Console:
|
15
|
+
* `!edit` - Edits a Ruby tempfile and loads the contents afterwards.
|
16
|
+
* `!cd` - Changes the current working directory and updates
|
17
|
+
`ENV['OLDPWD']`.
|
18
|
+
* `!export` - Sets `ENV` variables.
|
19
|
+
* Added an index to {Ronin::OS.version}.
|
20
|
+
* Refactored {Ronin::OS.predefine} using dm-is-predefined.
|
21
|
+
* Fixed a bug in {Ronin::UI::Console.setup} where the wrong binding was
|
22
|
+
being passed to Ripl.
|
23
|
+
|
1
24
|
### 1.1.0 / 2011-07-04
|
2
25
|
|
3
26
|
* Require env ~> 0.2.
|
data/README.md
CHANGED
@@ -72,9 +72,9 @@ Mercurial or Git.
|
|
72
72
|
[ronin-support](http://github.com/ronin-ruby/ronin-support#readme).
|
73
73
|
* Provides a customized Ruby Console with:
|
74
74
|
* Syntax highlighting.
|
75
|
-
* Tab-completion
|
76
|
-
* Auto-indentation
|
77
|
-
* Pretty-
|
75
|
+
* Tab-completion.
|
76
|
+
* Auto-indentation.
|
77
|
+
* Pretty-Printing (`pp`).
|
78
78
|
* `print_info`, `print_error`, `print_warning` and `print_debug`
|
79
79
|
output helper methods with color-output.
|
80
80
|
* Provides an extensible command-line interface based on
|
@@ -110,7 +110,7 @@ Update a specific Repositories:
|
|
110
110
|
|
111
111
|
$ ronin repos --update repo-name
|
112
112
|
|
113
|
-
Uninstall
|
113
|
+
Uninstall a specific Repositories:
|
114
114
|
|
115
115
|
$ ronin repos --uninstall repo-name
|
116
116
|
|
@@ -131,22 +131,22 @@ Remove a Database:
|
|
131
131
|
* [Ruby](http://www.ruby-lang.org/) >= 1.8.7
|
132
132
|
* [DataMapper](http://datamapper.org/):
|
133
133
|
* [dm-sqlite-adapter](http://github.com/datamapper/dm-sqlite-adapter#readme)
|
134
|
-
~> 1.1
|
134
|
+
~> 1.1
|
135
135
|
* [libsqlite3](http://sqlite.org/)
|
136
136
|
* [dm-core](http://github.com/datamapper/dm-core#readme)
|
137
|
-
~> 1.1
|
137
|
+
~> 1.1
|
138
138
|
* [dm-types](http://github.com/datamapper/dm-types#readme)
|
139
|
-
~> 1.1
|
139
|
+
~> 1.1
|
140
140
|
* [dm-migrations](http://github.com/datamapper/dm-migrations#readme)
|
141
|
-
~> 1.1
|
141
|
+
~> 1.1
|
142
142
|
* [dm-validations](http://github.com/datamapper/dm-validations#readme)
|
143
|
-
~> 1.1
|
143
|
+
~> 1.1
|
144
144
|
* [dm-aggregates](http://github.com/datamapper/dm-aggregates#readme)
|
145
|
-
~> 1.1
|
145
|
+
~> 1.1
|
146
146
|
* [dm-timestamps](http://github.com/datamapper/dm-timestamps#readme)
|
147
|
-
~> 1.1
|
147
|
+
~> 1.1
|
148
148
|
* [dm-is-predefined](http://github.com/postmodern/dm-is-predefined#readme)
|
149
|
-
~> 0.
|
149
|
+
~> 0.4
|
150
150
|
* [uri-query_params](http://github.com/postmodern/uri-query_params#readme)
|
151
151
|
~> 0.5, >= 0.5.2
|
152
152
|
* [open_namespace](http://github.com/postmodern/open_namespace#readme)
|
@@ -162,7 +162,7 @@ Remove a Database:
|
|
162
162
|
* [pullr](http://github.com/postmodern/pullr#readme)
|
163
163
|
~> 0.1, >= 0.1.2
|
164
164
|
* [ripl](https://github.com/cldwalker/ripl#readme)
|
165
|
-
~> 0.3
|
165
|
+
~> 0.3
|
166
166
|
* [ripl-multi_line](https://github.com/janlelis/ripl-multi_line#readme)
|
167
167
|
~> 0.2
|
168
168
|
* [ripl-auto_indent](https://github.com/janlelis/ripl-auto_indent#readme)
|
@@ -205,6 +205,12 @@ functionality.
|
|
205
205
|
[Ronin Gen](http://github.com/ronin-ruby/ronin-gen#readme) is a Ruby library
|
206
206
|
for Ronin that provides various generators.
|
207
207
|
|
208
|
+
### Ronin Scanners
|
209
|
+
|
210
|
+
[Ronin Scanners](http://github.com/ronin-ruby/ronin-scanners#readme)
|
211
|
+
is a Ruby library for Ronin that provides Ruby interfaces to
|
212
|
+
various third-party security scanners.
|
213
|
+
|
208
214
|
### Ronin SQL
|
209
215
|
|
210
216
|
[Ronin SQL](http://github.com/ronin-ruby/ronin-sql#readme) is a Ruby library
|
data/Rakefile
CHANGED
@@ -46,7 +46,7 @@ DataMapper::Visualizer::Rake::GraphVizTask.new(
|
|
46
46
|
ronin/organization
|
47
47
|
ronin/os_guess
|
48
48
|
ronin/os
|
49
|
-
ronin/
|
49
|
+
ronin/script/path
|
50
50
|
ronin/repository
|
51
51
|
ronin/port
|
52
52
|
ronin/service
|
@@ -58,6 +58,7 @@ DataMapper::Visualizer::Rake::GraphVizTask.new(
|
|
58
58
|
ronin/tcp_port
|
59
59
|
ronin/udp_port
|
60
60
|
ronin/url_scheme
|
61
|
+
ronin/url_query_param_name
|
61
62
|
ronin/url_query_param
|
62
63
|
ronin/url
|
63
64
|
ronin/user_name
|
data/gemspec.yml
CHANGED
@@ -13,12 +13,13 @@ has_yard: true
|
|
13
13
|
post_install_message: |
|
14
14
|
*************************************************************************
|
15
15
|
|
16
|
-
Thank you for installing Ronin
|
17
|
-
|
16
|
+
Thank you for installing Ronin!
|
17
|
+
|
18
|
+
To list the available commands:
|
18
19
|
|
19
20
|
$ ronin help
|
20
21
|
|
21
|
-
To
|
22
|
+
To start the Ronin Console:
|
22
23
|
|
23
24
|
$ ronin
|
24
25
|
|
@@ -35,20 +36,20 @@ post_install_message: |
|
|
35
36
|
required_ruby_version: ">= 1.8.7"
|
36
37
|
|
37
38
|
dependencies:
|
38
|
-
# DataMapper adapters
|
39
|
-
dm-sqlite-adapter: ~> 1.1
|
40
|
-
# DataMapper dependencies
|
41
|
-
dm-core: ~> 1.1
|
42
|
-
dm-types: ~> 1.1
|
43
|
-
dm-constraints: ~> 1.1
|
44
|
-
dm-migrations: ~> 1.1
|
45
|
-
dm-validations: ~> 1.1
|
46
|
-
dm-serializer: ~> 1.1
|
47
|
-
dm-aggregates: ~> 1.1
|
48
|
-
dm-timestamps: ~> 1.1
|
49
|
-
# DataMapper plugins
|
50
|
-
dm-is-predefined: ~> 0.
|
51
|
-
# Library dependencies
|
39
|
+
# DataMapper adapters:
|
40
|
+
dm-sqlite-adapter: ~> 1.1
|
41
|
+
# DataMapper dependencies:
|
42
|
+
dm-core: ~> 1.1
|
43
|
+
dm-types: ~> 1.1
|
44
|
+
dm-constraints: ~> 1.1
|
45
|
+
dm-migrations: ~> 1.1
|
46
|
+
dm-validations: ~> 1.1
|
47
|
+
dm-serializer: ~> 1.1
|
48
|
+
dm-aggregates: ~> 1.1
|
49
|
+
dm-timestamps: ~> 1.1
|
50
|
+
# DataMapper plugins:
|
51
|
+
dm-is-predefined: ~> 0.4
|
52
|
+
# Library dependencies:
|
52
53
|
uri-query_params: ~> 0.5, >= 0.5.2
|
53
54
|
open_namespace: ~> 0.3
|
54
55
|
parameters: ~> 0.2, >= 0.2.3
|
@@ -0,0 +1,147 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2006-2011 Hal Brodigan (postmodern.mod3 at gmail.com)
|
3
|
+
#
|
4
|
+
# This file is part of Ronin.
|
5
|
+
#
|
6
|
+
# Ronin is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# Ronin is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with Ronin. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'ronin/ui/console/commands'
|
21
|
+
require 'ronin/address'
|
22
|
+
require 'ronin/ip_address'
|
23
|
+
require 'ronin/host_name'
|
24
|
+
require 'ronin/email_address'
|
25
|
+
require 'ronin/url'
|
26
|
+
|
27
|
+
require 'set'
|
28
|
+
require 'env'
|
29
|
+
|
30
|
+
complete(:on => /^\![a-zA-Z]\w*/) do |cmd|
|
31
|
+
prefix = cmd[1..-1]
|
32
|
+
glob = "#{prefix}*"
|
33
|
+
paths = Set[]
|
34
|
+
|
35
|
+
# search through $PATH for similar program names
|
36
|
+
Env.paths.each do |dir|
|
37
|
+
Pathname.glob(dir.join(glob)) do |path|
|
38
|
+
if (path.file? && path.executable?)
|
39
|
+
paths << "!#{path.basename}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# add the black-listed keywords last
|
45
|
+
Ronin::UI::Console::Commands::BLACKLIST.each do |keyword|
|
46
|
+
paths << "!#{keyword}" if keyword.start_with?(prefix)
|
47
|
+
end
|
48
|
+
|
49
|
+
paths
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# {URL} completion in the context of URLs.
|
54
|
+
#
|
55
|
+
#
|
56
|
+
# http://www.example.com/in[TAB][TAB] => http://www.example.com/index.html
|
57
|
+
#
|
58
|
+
complete(:anywhere => /[a-z]+:\/\/([^:\/\?]+(:\d+)?(\/[^\?;]*(\?[^\?;]*)?)?)?/) do |url|
|
59
|
+
match = url.match(/([a-z]+):\/\/([^:\/\?]+)(:\d+)?(\/[^\?;]*)?(\?[^\?;]*)?/)
|
60
|
+
|
61
|
+
query = Ronin::URL.all('scheme.name' => match[1])
|
62
|
+
|
63
|
+
if match[2]
|
64
|
+
unless (match[4] || match[3])
|
65
|
+
query = query.all('host_name.address.like' => "#{match[2]}%")
|
66
|
+
else
|
67
|
+
query = query.all('host_name.address' => match[2])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if match[3]
|
72
|
+
query = query.all('port.number' => match[3])
|
73
|
+
end
|
74
|
+
|
75
|
+
if match[4]
|
76
|
+
unless match[5]
|
77
|
+
query = query.all(:path.like => "#{match[4]}%")
|
78
|
+
else
|
79
|
+
query = query.all(:path => match[4])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if match[5]
|
84
|
+
params = URI::QueryParams.parse(match[5][1..-1]).to_a
|
85
|
+
|
86
|
+
params[0..-2].each do |name,value|
|
87
|
+
query = query.all(
|
88
|
+
'query_params.name.name' => name,
|
89
|
+
'query_params.value' => value
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
if (param = params.last)
|
94
|
+
if param[1].empty?
|
95
|
+
query = query.all('query_params.name.name.like' => "#{param[0]}%")
|
96
|
+
else
|
97
|
+
query = query.all(
|
98
|
+
'query_params.name.name' => param[0],
|
99
|
+
'query_params.value.like' => "#{param[1]}%"
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
query
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# {IPAddress} completion:
|
110
|
+
#
|
111
|
+
# 192.168.[TAB][TAB] => 192.168.0.1
|
112
|
+
#
|
113
|
+
complete(:anywhere => /(\d{1,3}\.){1,3}\d{,2}/) do |addr|
|
114
|
+
Ronin::Address.all(:address.like => "#{addr}%")
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# {HostName} completion:
|
119
|
+
#
|
120
|
+
# www.[TAB][TAB] => www.example.com
|
121
|
+
#
|
122
|
+
complete(:anywhere => /[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*/) do |host|
|
123
|
+
Ronin::HostName.all(:address.like => "#{host}%")
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# {EmailAddress} completeion:
|
128
|
+
#
|
129
|
+
# alice@[TAB][TAB] => alice@example.com
|
130
|
+
#
|
131
|
+
complete(:anywhere => /[a-zA-Z0-9\._-]+@[a-zA-Z0-9\._-]*/) do |email|
|
132
|
+
user, host = email.split('@',2)
|
133
|
+
|
134
|
+
Ronin::EmailAddress.all(
|
135
|
+
'user_name.name' => user,
|
136
|
+
'host_name.address.like' => "#{host}%"
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Path completion.
|
142
|
+
#
|
143
|
+
# /etc/pa[TAB][TAB] => /etc/passwd
|
144
|
+
#
|
145
|
+
complete(:anywhere => /\/([^\/]+\/)*[^\/]*/) do |path|
|
146
|
+
Dir["#{path}*"]
|
147
|
+
end
|
data/lib/ronin/auto_load.rb
CHANGED
@@ -22,46 +22,48 @@ require 'open_namespace'
|
|
22
22
|
module Ronin
|
23
23
|
#
|
24
24
|
# When included into other namespaces, it allows for auto-loading Classes
|
25
|
-
# or Modules via {#const_missing}.
|
25
|
+
# or Modules via {ClassMethods#const_missing}.
|
26
26
|
#
|
27
27
|
# @since 1.1.0
|
28
28
|
#
|
29
29
|
module AutoLoad
|
30
30
|
def self.included(base)
|
31
31
|
base.send :include, OpenNamespace
|
32
|
-
base.send :extend,
|
32
|
+
base.send :extend, ClassMethods
|
33
33
|
end
|
34
34
|
|
35
|
-
|
35
|
+
module ClassMethods
|
36
|
+
protected
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
38
|
+
#
|
39
|
+
# Transparently auto-loads Classes and Modules from their respective
|
40
|
+
# files using [OpenNamespace](http://rubydoc.info/gems/open_namespace).
|
41
|
+
#
|
42
|
+
# @param [String, Symbol] name
|
43
|
+
# The name of the Class or Module to auto-load.
|
44
|
+
#
|
45
|
+
# @return [Class, Module]
|
46
|
+
# The loaded Class or Module.
|
47
|
+
#
|
48
|
+
# @raise [NameError]
|
49
|
+
# The Class or Module could not be found.
|
50
|
+
#
|
51
|
+
# @since 1.1.0
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
#
|
55
|
+
def const_missing(name)
|
56
|
+
const = super(name)
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
58
|
+
if Object.const_defined?('DataMapper')
|
59
|
+
# if the loaded Class is a DataMapper Resource, re-finalize
|
60
|
+
if const < DataMapper::Resource
|
61
|
+
DataMapper.finalize
|
62
|
+
end
|
61
63
|
end
|
62
|
-
end
|
63
64
|
|
64
|
-
|
65
|
+
return const
|
66
|
+
end
|
65
67
|
end
|
66
68
|
end
|
67
69
|
end
|
@@ -17,5 +17,95 @@
|
|
17
17
|
# along with Ronin. If not, see <http://www.gnu.org/licenses/>.
|
18
18
|
#
|
19
19
|
|
20
|
-
require 'ronin/model
|
21
|
-
require 'ronin/
|
20
|
+
require 'ronin/model'
|
21
|
+
require 'ronin/author'
|
22
|
+
|
23
|
+
module Ronin
|
24
|
+
module Model
|
25
|
+
#
|
26
|
+
# Adds an `authors` relationship between a model and the {Author} model.
|
27
|
+
#
|
28
|
+
module HasAuthors
|
29
|
+
#
|
30
|
+
# Adds the `authors` relationship and {ClassMethods} to the model.
|
31
|
+
#
|
32
|
+
# @param [Class] base
|
33
|
+
# The model.
|
34
|
+
#
|
35
|
+
# @api semipublic
|
36
|
+
#
|
37
|
+
def self.included(base)
|
38
|
+
base.send :include, Model, InstanceMethods
|
39
|
+
base.send :extend, ClassMethods
|
40
|
+
|
41
|
+
base.module_eval do
|
42
|
+
# The authors associated with the model.
|
43
|
+
has 0..n, :authors, Ronin::Author, :through => DataMapper::Resource
|
44
|
+
|
45
|
+
Ronin::Author.has 0..n, self.relationship_name,
|
46
|
+
:through => DataMapper::Resource,
|
47
|
+
:model => self
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Class methods that are added when {HasAuthors} is included into a
|
53
|
+
# model.
|
54
|
+
#
|
55
|
+
module ClassMethods
|
56
|
+
#
|
57
|
+
# Finds all resources associated with a given author.
|
58
|
+
#
|
59
|
+
# @param [String] name
|
60
|
+
# The name of the author.
|
61
|
+
#
|
62
|
+
# @return [Array<Model>]
|
63
|
+
# The resources written by the author.
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
#
|
67
|
+
def written_by(name)
|
68
|
+
all('authors.name.like' => "%#{name}%")
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Finds all resources associated with a given organization.
|
73
|
+
#
|
74
|
+
# @param [String] name
|
75
|
+
# The name of the organization.
|
76
|
+
#
|
77
|
+
# @return [Array<Model>]
|
78
|
+
# The resources associated with the organization.
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
#
|
82
|
+
def written_for(name)
|
83
|
+
all('authors.organization.like' => "%#{name}%")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Instance methods that are added when {HasAuthors} is included into a
|
89
|
+
# model.
|
90
|
+
#
|
91
|
+
module InstanceMethods
|
92
|
+
#
|
93
|
+
# Adds a new author to the resource.
|
94
|
+
#
|
95
|
+
# @param [Hash] attributes
|
96
|
+
# Additional attributes to create the new author.
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# author :name => 'Anonymous',
|
100
|
+
# :email => 'anon@example.com',
|
101
|
+
# :organization => 'Anonymous LLC'
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
#
|
105
|
+
def author(attributes)
|
106
|
+
self.authors.new(attributes)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|