wrangler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,142 @@
1
+ module Wrangler
2
+ # handles notifying (sending emails) for the wrangler gem. configuration for
3
+ # the class is set in an initializer via the configure() method, e.g.
4
+ #
5
+ # Wrangler::ExceptionNotifier.configure do |config|
6
+ # config[:key] = value
7
+ # end
8
+ #
9
+ # see README or source code for possible values and default settings
10
+ #-----------------------------------------------------------------------------
11
+ class ExceptionNotifier < ActionMailer::Base
12
+ # the default configuration
13
+ @@config ||= {
14
+ # who the emails will be coming from. if nil or missing or empty string,
15
+ # effectively disables email notification
16
+ :from_address => '',
17
+ # array of addresses that the emails will be sent to. if nil or missing
18
+ # or empty array, effectively disables email notification.
19
+ :recipient_addresses => [],
20
+ # what will show up at the beginning of the subject line for each email
21
+ # sent note: will be preceded by "[<app_name (if any)>...", where app_name
22
+ # is the :app_name config value from ExceptionHandler (or explicit
23
+ # proc_name given to notify_on_error() method)
24
+ :subject_prefix => "#{(defined?(Rails) ? Rails.env : RAILS_ENV).capitalize} ERROR",
25
+ # can use this to define app-specific mail views using the same data
26
+ # made available in exception_notification()
27
+ :mailer_template_root => File.join(WRANGLER_ROOT, 'views')
28
+ }
29
+
30
+ cattr_accessor :config
31
+
32
+ def self.configure(&block)
33
+ yield @@config
34
+ end
35
+
36
+ self.template_root = config[:mailer_template_root]
37
+
38
+ # form and send the notification email (note: this gets called indirectly
39
+ # when you call ExceptionNotifier.deliver_exception_notification())
40
+ #
41
+ # arguments:
42
+ # - exception: the exception that was raised
43
+ # - proc_name: the name of the process in which the exception arised
44
+ # - backtrace: the stack trace from the exception (passing in excplicitly
45
+ # because serializing the exception does not preserve the
46
+ # backtrace (in case delayed_job is used to async the email)
47
+ # - status_code: the (string) http status code that the exception has been
48
+ # mapped to. Optional, but no default is provided, so
49
+ # no status code info will be contained in the email.
50
+ # - request_data: hash with relevant data from the request object.
51
+ # Optional, but if not present, then assumed the exception
52
+ # was not due to an http request and thus no request
53
+ # data will be contained in the email.
54
+ # - request: the original request that resulted in the exception. may
55
+ # be nil and MUST be nil if calling this method with
56
+ # delayed_job. Optional.
57
+ #---------------------------------------------------------------------------
58
+ def exception_notification(exception, proc_name, backtrace,
59
+ status_code = nil,
60
+ request_data = nil,
61
+ request = nil)
62
+
63
+ # don't try to send email if there are no from or recipient addresses
64
+ if config[:from_address].nil? ||
65
+ config[:from_address].empty? ||
66
+ config[:recipient_addresses].nil? ||
67
+ config[:recipient_addresses].empty?
68
+
69
+ return nil
70
+ end
71
+
72
+ ensure_session_loaded(request)
73
+
74
+ # NOTE: be very careful pulling data out of request in the view...it is
75
+ # NOT cleaned, and may contain private data (e.g. passwords), so
76
+ # scrutinize any use of @request in the views!
77
+
78
+ body_hash =
79
+ { :exception => exception,
80
+ :backtrace => backtrace,
81
+ :status_code => status_code,
82
+ :request_data => request_data,
83
+ :request => request
84
+ }
85
+
86
+ body_hash.merge! extract_data_from_request_data(request_data)
87
+ from config[:from_address]
88
+ recipients config[:recipient_addresses]
89
+ subject "[#{proc_name + (proc_name ? ' ' : '')}" +
90
+ "#{config[:subject_prefix]}] " +
91
+ "#{exception.class.name}: " +
92
+ "#{exception.message.inspect}"
93
+ body body_hash
94
+ sent_on Time.now
95
+ content_type 'text/plain'
96
+ end
97
+
98
+ # helper to force loading of session data in case of lazy loading (Rails
99
+ # 2.3). if the argument isn't a request object, then don't bother, cause
100
+ # it won't have session.
101
+ #---------------------------------------------------------------------------
102
+ def ensure_session_loaded(request)
103
+ request.session.inspect if !request.nil? && request.respond_to?(:session)
104
+ true
105
+ end
106
+
107
+ # extract relevant (and serializable) data from a request object
108
+ #---------------------------------------------------------------------------
109
+ def extract_data_from_request_data(request_data)
110
+ if request_data
111
+ { :host => host_from_request_data(request_data),
112
+ :protocol => protocol_from_request_data(request_data),
113
+ :uri => uri_from_request_data(request_data)
114
+ }
115
+ else
116
+ {}
117
+ end
118
+ end
119
+
120
+ # extract the host from request object
121
+ #---------------------------------------------------------------------------
122
+ def host_from_request_data(request_data)
123
+ request_data['HTTP_X_REAL_IP'] ||
124
+ request_data['HTTP_X_FORWARDED_HOST'] ||
125
+ request_data['HTTP_HOST']
126
+ end
127
+
128
+ # extract protocol from request object
129
+ #---------------------------------------------------------------------------
130
+ def protocol_from_request_data(request_data)
131
+ request_data['SERVER_PROTOCOL']
132
+ end
133
+
134
+ # extract URI from request object
135
+ #---------------------------------------------------------------------------
136
+ def uri_from_request_data(request_data)
137
+ request_data['REQUEST_URI']
138
+ end
139
+
140
+ end
141
+
142
+ end
@@ -0,0 +1,63 @@
1
+ # a bunch of handy exceptions. using the Http ones will require rails
2
+ module Wrangler
3
+
4
+ # base class for all the http-related exception classes
5
+ #-----------------------------------------------------------------------------
6
+ class HttpStatusError < Exception
7
+ def initialize(status_code, addl_msg = nil)
8
+ @status_code = status_code
9
+ @addl_msg = addl_msg
10
+
11
+ full_message = self.class.status_to_msg(status_code)
12
+ full_message += addl_msg unless addl_msg.nil?
13
+
14
+ super(full_message)
15
+ end
16
+
17
+ attr_reader :status_code, :addl_msg
18
+
19
+ # accepts either a Fixnum status code or a symbol representation of a
20
+ # status code (e.g. 404 or :not_found)
21
+ # yes, this is total duplication of code in
22
+ # action_controller/status_codes.rb, but it's been made a private instance
23
+ # method in the module.
24
+ #---------------------------------------------------------------------------
25
+ def self.status_to_msg(status)
26
+ case status
27
+ when Fixnum then
28
+ "#{status} #{ActionController::StatusCodes::STATUS_CODES[status]}".strip
29
+ when Symbol then
30
+ status_to_msg(ActionController::StatusCodes::SYMBOL_TO_STATUS_CODE[status] ||
31
+ "500 Unknown Status #{status.inspect}")
32
+ else
33
+ status.to_s
34
+ end
35
+ end
36
+ end
37
+
38
+
39
+ class HttpUnauthorized < HttpStatusError
40
+ def initialize(msg = nil); super(401, msg); end
41
+ end
42
+
43
+ class HttpNotFound < HttpStatusError
44
+ def initialize(msg = nil); super(404, msg); end
45
+ end
46
+
47
+ class HttpNotAcceptable < HttpStatusError
48
+ def initialize(msg = nil); super(406, msg); end
49
+ end
50
+
51
+ class HttpInternalServerError < HttpStatusError
52
+ def initialize(msg = nil); super(500, msg); end
53
+ end
54
+
55
+ class HttpNotImplemented < HttpStatusError
56
+ def initialize(msg = nil); super(501, msg); end
57
+ end
58
+
59
+ class HttpServiceUnavailable < HttpStatusError
60
+ def initialize(msg = nil); super(503, msg); end
61
+ end
62
+
63
+ end
@@ -0,0 +1,95 @@
1
+ module Wrangler
2
+ WRANGLER_ROOT = "#{File.dirname(__FILE__)}/../.."
3
+
4
+ # make all of these instance methods act as module functions as well
5
+ # (any instance method below this gets added as a module function as well)
6
+ module_function
7
+
8
+ # utility to determine if a class has another class as its ancestor.
9
+ #
10
+ # returns the ancestor class (if any) that is found to be the ancestor
11
+ # of klass (will return the nearest ancestor in other_klasses).
12
+ # returns nil or false otherwise.
13
+ #
14
+ # arguments:
15
+ # - klass: the class we're determining whether it has one of the other
16
+ # classes as an ancestor
17
+ # - other_klasses: a Class, an Array (or any other container that responds
18
+ # to include?() ) of Classes
19
+ #-----------------------------------------------------------------------------
20
+ def class_has_ancestor?(klass, other_klasses)
21
+ return nil if !klass.is_a?(Class)
22
+
23
+ other_klasses = [other_klasses] if other_klasses.is_a?(Class)
24
+
25
+ current_klass = klass
26
+ while current_klass
27
+ return current_klass if other_klasses.include?(current_klass)
28
+ current_klass = current_klass.superclass
29
+ end
30
+
31
+ return false
32
+ end
33
+
34
+
35
+ # given an array of search directory strings (or a single directory string),
36
+ # searches for files matching pattern.
37
+ #
38
+ # pattern expressed in cmd line wildcards...like "*.rb" or "foo.?"...
39
+ # and may contain subdirectories.
40
+ #---------------------------------------------------------------------------
41
+ def find_file_matching_pattern(search_dirs, pattern)
42
+ search_dirs = [search_dirs] unless search_dirs.is_a?(Array)
43
+
44
+ search_dirs.each do |d|
45
+ matches = Dir.glob(File.join(d, pattern))
46
+ return matches.first if matches.size > 0
47
+ end
48
+ return nil
49
+ end
50
+
51
+ # log the exception using logger if available. if object does not have a
52
+ # logger, will just puts()
53
+ #-----------------------------------------------------------------------------
54
+ def log_exception(exception, request_data = nil, status_code = nil)
55
+ msgs = []
56
+
57
+ msgs << "An exception was caught (#{exception.class.name}):"
58
+ msgs << exception.message
59
+ unless request_data.blank?
60
+ msgs << "Request params were:"
61
+ msgs << request_data.inspect
62
+ end
63
+ unless status_code.blank?
64
+ msgs << "Handling with status code: #{status_code}"
65
+ end
66
+ unless exception.backtrace.blank?
67
+ msgs << exception.backtrace.join("\n ")
68
+ end
69
+
70
+ log_error msgs
71
+ end
72
+
73
+ # handles logging error messages, using logger if available and puts otherwise
74
+ #-----------------------------------------------------------------------------
75
+ def log_error(msgs)
76
+ unless msgs.is_a?(Array)
77
+ msgs = [msgs]
78
+ end
79
+
80
+ msgs.each do |m|
81
+ if respond_to?(:logger)
82
+ logger.error m
83
+ else
84
+ puts m
85
+ end
86
+ end
87
+ end
88
+
89
+ # shorthand access to the exception handling config
90
+ #-----------------------------------------------------------------------------
91
+ def config
92
+ Wrangler::ExceptionHandler.config
93
+ end
94
+
95
+ end
data/lib/wrangler.rb ADDED
@@ -0,0 +1,284 @@
1
+ require 'wrangler/wrangler_helper.rb'
2
+ require 'wrangler/exception_handler.rb'
3
+ require 'wrangler/exception_notifier.rb'
4
+
5
+ module Wrangler
6
+
7
+ def self.included(base)
8
+ # only add in the controller-specific methods if the including class is one
9
+ if class_has_ancestor?(base, ActionController::Base)
10
+ base.send(:include, ControllerMethods)
11
+
12
+ # conditionally including these methods (each wrapped in a separate
13
+ # module) based on the configuration of whether to handle exceptions in
14
+ # the given environment or not. this allows the default implementation
15
+ # of the two rescue methods to run when Wrangler-based exception handling
16
+ # is disabled.
17
+
18
+ if Wrangler::ExceptionHandler.config[:handle_public_errors]
19
+ Rails.logger.info "Configuring #{base.name} with Wrangler's rescue_action_in_public"
20
+ base.send(:include, PublicControllerMethods)
21
+ else
22
+ Rails.logger.info "NOT Configuring #{base.name} with Wrangler's rescue_action_in_public"
23
+ end
24
+
25
+ if Wrangler::ExceptionHandler.config[:handle_local_errors]
26
+ Rails.logger.info "Configuring #{base.name} with Wrangler's rescue_action_locally"
27
+ base.send(:include, LocalControllerMethods)
28
+ else
29
+ Rails.logger.info "NOT configuring #{base.name} with Wrangler's rescue_action_locally"
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ # methods to only be included into a controller if Wrangler is configured to
36
+ # handle exceptions for public reqeusts. (Conditionally included into
37
+ # controllers in the Wrangler::included() method).
38
+ #-----------------------------------------------------------------------------
39
+ module PublicControllerMethods
40
+ # override default behavior and let Wrangler handle the exception for
41
+ # public (non-local) requests.
42
+ #---------------------------------------------------------------------------
43
+ def rescue_action_in_public(exception)
44
+ handle_exception(exception, :request => request,
45
+ :render_errors => true)
46
+ end
47
+ end
48
+
49
+
50
+ # methods to only be included into a controller if Wrangler is configured to
51
+ # handle exceptions for local reqeusts. (Conditionally included into
52
+ # controllers in the Wrangler::included() method).
53
+ #-----------------------------------------------------------------------------
54
+ module LocalControllerMethods
55
+ # override default behavior and let Wrangler handle the exception for
56
+ # local requests.
57
+ #---------------------------------------------------------------------------
58
+ def rescue_action_locally(exception)
59
+ handle_exception(exception, :request => request,
60
+ :render_errors => true)
61
+ end
62
+ end
63
+
64
+
65
+ # module of instance methods to be added to the class including Wrangler
66
+ # only if the including class is a rails controller class
67
+ #-----------------------------------------------------------------------------
68
+ module ControllerMethods
69
+
70
+ # called by rails if the exception has already been handled (e.g. by
71
+ # calling the rescue_from method in a controller and rendering a response)
72
+ #---------------------------------------------------------------------------
73
+ def rescue_with_handler(exception)
74
+ to_return = super
75
+ if to_return &&
76
+ (
77
+ (local_request? && Wrangler::ExceptionHandler.config[:handle_local_errors]) ||
78
+ (!local_request? && Wrangler::ExceptionHandler.config[:handle_public_errors])
79
+ )
80
+
81
+ handle_exception(exception, :request => request,
82
+ :render_errors => false)
83
+ end
84
+ to_return
85
+ end
86
+
87
+
88
+ # extract a hash of relevant (and serializable) parameters from a request
89
+ #---------------------------------------------------------------------------
90
+ def request_data_from_request(request)
91
+ return nil if request.nil?
92
+
93
+ request_data = {}
94
+ request.env.each_pair do |k,v|
95
+ next if skip_request_env?(k)
96
+
97
+ if self.respond_to?(:filter_parameters)
98
+ request_data.merge! self.send(:filter_parameters, k => v)
99
+ else
100
+ request_data.merge! k => v
101
+ end
102
+ end
103
+
104
+ params = {}
105
+ if self.respond_to?(:filter_parameters)
106
+ params.merge!(
107
+ filter_parameters(request.env['action_controller.request.query_parameters'])
108
+ )
109
+ params.merge!(
110
+ filter_parameters(request.env['action_controller.request.request_parameters'])
111
+ )
112
+ else
113
+ params.merge! request.env['action_controller.request.query_parameters']
114
+ params.merge! request.env['action_controller.request.request_parameters']
115
+ end
116
+
117
+ request_data.merge! :params => params unless params.blank?
118
+
119
+ return request_data
120
+ end
121
+
122
+
123
+ # determine if the request env param should be ommitted from the request
124
+ # data object, as specified in config (either for aesthetic reasons or
125
+ # because the param won't serialize well).
126
+ #---------------------------------------------------------------------------
127
+ def skip_request_env?(request_param)
128
+ skip_env = false
129
+ Wrangler::ExceptionHandler.config[:request_env_to_skip].each do |pattern|
130
+ if (pattern.is_a?(String) && pattern == request_param) ||
131
+ (pattern.is_a?(Regexp) && pattern =~ request_param)
132
+ skip_env = true
133
+ break
134
+ end
135
+ end
136
+
137
+ return skip_env
138
+ end
139
+
140
+
141
+ # select the proper file to render and do so. if the usual places don't
142
+ # turn up an appropriate template (see README), then fall back on
143
+ # an app-specific default error page or the ultimate back up gem default
144
+ # page.
145
+ #---------------------------------------------------------------------------
146
+ def render_error_template(exception, status_code)
147
+ file_path = get_view_path_for_exception(exception, status_code)
148
+
149
+ # if that didn't work, fall back on configured app-specific default
150
+ if file_path.blank? || !File.exists?(file_path)
151
+ file_path = config[:default_error_template]
152
+
153
+ log_error(["Could not find an error template in the usual places " +
154
+ "for exception #{exception.class}, status code " +
155
+ "#{status_code}.",
156
+ "Trying to default to app-specific default: '#{file_path}'"])
157
+ end
158
+
159
+ # as a last resort, just render the gem's 500 error
160
+ if file_path.blank? || !File.exists?(file_path)
161
+ file_path = config[:absolute_last_resort_default_error_template]
162
+
163
+ log_error("Still no template found. Using gem default of " +
164
+ file_path)
165
+ end
166
+
167
+ log_error("Will render error template: '#{file_path}'")
168
+
169
+ render :file => file_path,
170
+ :status => status_code
171
+ end
172
+
173
+
174
+ # select the appropriate view path for the exception/status code. see
175
+ # README or the code for the different attempts that are made to find
176
+ # a template.
177
+ #---------------------------------------------------------------------------
178
+ def get_view_path_for_exception(exception, status_code)
179
+
180
+ # maintenance note: this method has lots of RETURN statements, so be
181
+ # a little careful, but basically, it returns as soon as it finds a
182
+ # file match, so there shouldn't be any processing to perform after
183
+ # a match is found. any such logic does not belong in this method
184
+
185
+ if exception.is_a?(Class)
186
+ exception_class = exception
187
+ else
188
+ exception_class = exception.class
189
+ end
190
+
191
+ # Note: this converts "::" to "/", so views need to be nested under
192
+ # exceptions' modules if appropriate
193
+ exception_filename_root = exception_class.name.underscore
194
+
195
+ template_mappings = nil
196
+ case request.format
197
+ when /html/
198
+ response_format = 'html'
199
+ template_mappings = config[:error_class_html_templates]
200
+ when /js/
201
+ response_format = 'js'
202
+ template_mappings = config[:error_class_js_templates]
203
+ when /xml/
204
+ 'xml'
205
+ end
206
+ format_extension_pattern = ".#{response_format || ''}*"
207
+
208
+ if template_mappings
209
+
210
+ # search for direct mapping from exception name to error template
211
+
212
+ if template_mappings[exception_class]
213
+ error_file = template_mappings[exception_class]
214
+
215
+ return error_file if File.exists?(error_file)
216
+
217
+ log_error("Found mapping from exception class " +
218
+ "#{exception_class.name} to error file '#{error_file}', " +
219
+ "but error file was not found")
220
+ end
221
+
222
+ # search for mapping from an ancestor class to error template
223
+
224
+ ancestor_class =
225
+ Wrangler::class_has_ancestor?(exception_class.superclass,
226
+ template_mappings)
227
+
228
+ if ancestor_class
229
+ error_file = template_mappings[ancestor_class]
230
+
231
+ return error_file if File.exists?(error_file)
232
+
233
+ log_error("Found mapping from ancestor exception class " +
234
+ "#{ancestor_class.name} to error file '#{error_file}', " +
235
+ "but error file was not found")
236
+ end
237
+
238
+ end # end if template_mappings
239
+
240
+ # search for a file named after the exception in one of the search dirs
241
+
242
+ search_paths = [ config[:error_template_dir],
243
+ File.join(RAILS_ROOT, 'public'),
244
+ File.join(WRANGLER_ROOT, 'rails', 'app', 'views', 'wrangler')
245
+ ]
246
+
247
+ # find files in specified directory like 'exception_class_name.format', e.g.
248
+ # standard_error.html or standard_error.js.erb
249
+ exception_pattern = "#{exception_filename_root}#{format_extension_pattern}"
250
+ file_path = find_file_matching_pattern(search_paths, exception_pattern)
251
+
252
+ return file_path if file_path
253
+
254
+ # search for a file named after the error status code in search dirs
255
+
256
+ status_code_pattern = "#{status_code}#{format_extension_pattern}"
257
+ file_path = find_file_matching_pattern(search_paths, status_code_pattern)
258
+
259
+ return file_path if file_path
260
+
261
+ # search for a file named after ancestors of the exception in search dirs
262
+
263
+ # look through exception's entire ancenstry to see if there's a matching
264
+ # template in the search directories
265
+ curr_ancestor = exception_class.superclass
266
+ while curr_ancestor
267
+ # find files in specified directory like 'exception_class_name.format', e.g.
268
+ # standard_error.html or standard_error.js.erb
269
+ exception_pattern =
270
+ "#{curr_ancestor.name.underscore}#{format_extension_pattern}"
271
+ file_path = find_file_matching_pattern(search_paths, exception_pattern)
272
+
273
+ return file_path if file_path
274
+
275
+ curr_ancestor = curr_ancestor.superclass
276
+ end
277
+
278
+ # didn't find anything
279
+ return nil
280
+ end
281
+
282
+ end # end ControllerMethods module
283
+
284
+ end # end Wrangler module
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Unable to Process Requested Data</h1>
4
+
5
+ <p>Reloading the page will probably not help.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Not Authorized</h1>
4
+
5
+ <p>You are not authorized to access this resource. You may be either not logged in at all, or not logged in as the appropriate user.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Access Restricted</h1>
4
+
5
+ <p>If you need access to this resource please contact support or an administrator.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The page you were looking for doesn't exist.</h1>
4
+
5
+ <p>You may have mistyped the address or the page may have moved.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Invalid Method.</h1>
4
+
5
+ <p>This resource only accepts certain HTTP methods.</p>
6
+ </div>
@@ -0,0 +1,7 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Requested resource is no longer available</h1>
4
+
5
+ <p>Please remove bookmarks to this resource.</p>
6
+ <p>If you arrived here via a link from somewhere else please inform them of the broken link.</p>
7
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Unprocessable Request</h1>
4
+
5
+ <p>The request was well-formed but was unable to be followed due to semantic errors.</p>
6
+ </div>
@@ -0,0 +1,7 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The change you wanted was rejected because the resource is locked</h1>
4
+
5
+ <p>It might become available again soon, if you try again!</p>
6
+ <p>Maybe you tried to change something you didn't have access to.</p>
7
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>We're sorry, but something went wrong.</h1>
4
+
5
+ <p>We've been notified about this issue and we'll take a look at it shortly.</p>
6
+ </div>
@@ -0,0 +1,8 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Not Implemented</h1>
4
+
5
+ <p>Please remove bookmarks to this resource.</p>
6
+ <p>You have tried to access a feature that has not been implemented.</p>
7
+ <p>If you arrived here via a link from somewhere else please inform them of the broken link.</p>
8
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The server is temporarily unavailable.</h1>
4
+
5
+ <p>The resource you requested is temporarilly unavailable due to server overload, maintenance, or other downtime. Generally, this is a temporary state.</p>
6
+ </div>