jets 1.7.2 → 1.8.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +1 -1
  4. data/jets.gemspec +2 -0
  5. data/lib/jets.rb +1 -0
  6. data/lib/jets/application.rb +45 -21
  7. data/lib/jets/aws_services.rb +2 -0
  8. data/lib/jets/aws_services/s3_bucket.rb +26 -0
  9. data/lib/jets/booter.rb +82 -2
  10. data/lib/jets/builders/code_builder.rb +20 -7
  11. data/lib/jets/builders/handler_generator.rb +20 -6
  12. data/lib/jets/cfn/builders/base_child_builder.rb +5 -17
  13. data/lib/jets/cfn/builders/parent_builder.rb +1 -1
  14. data/lib/jets/commands/build.rb +6 -0
  15. data/lib/jets/commands/deploy.rb +10 -0
  16. data/lib/jets/commands/templates/skeleton/config/environments/development.rb +4 -1
  17. data/lib/jets/commands/templates/skeleton/config/environments/production.rb +6 -1
  18. data/lib/jets/commands/templates/skeleton/config/environments/test.rb +7 -0
  19. data/lib/jets/controller/base.rb +1 -2
  20. data/lib/jets/controller/rendering/rack_renderer.rb +11 -3
  21. data/lib/jets/core.rb +5 -72
  22. data/lib/jets/internal/app/controllers/jets/mailers_controller.rb +97 -0
  23. data/lib/jets/internal/app/helpers/jets/mailers_helper.rb +9 -0
  24. data/lib/jets/internal/app/shared/functions/jets/s3_bucket_config.rb +43 -0
  25. data/lib/jets/internal/app/views/jets/mailers/email.html.erb +145 -0
  26. data/lib/jets/internal/app/views/jets/mailers/index.html.erb +8 -0
  27. data/lib/jets/internal/app/views/jets/mailers/mailer.html.erb +6 -0
  28. data/lib/jets/job/base.rb +10 -0
  29. data/lib/jets/job/dsl.rb +2 -0
  30. data/lib/jets/job/dsl/s3_event.rb +36 -0
  31. data/lib/jets/job/dsl/sns_event.rb +8 -0
  32. data/lib/jets/job/s3_event_helper.rb +13 -0
  33. data/lib/jets/lambda/dsl.rb +11 -1
  34. data/lib/jets/mailer.rb +51 -0
  35. data/lib/jets/resource/child_stack/app_class.rb +6 -15
  36. data/lib/jets/resource/child_stack/shared.rb +3 -1
  37. data/lib/jets/resource/events/rule.rb +1 -1
  38. data/lib/jets/resource/lambda/event_source_mapping.rb +1 -1
  39. data/lib/jets/resource/permission.rb +1 -1
  40. data/lib/jets/resource/replacer.rb +8 -0
  41. data/lib/jets/resource/s3.rb +3 -17
  42. data/lib/jets/resource/s3/bucket.rb +24 -0
  43. data/lib/jets/resource/sns.rb +1 -0
  44. data/lib/jets/resource/sns/subscription.rb +1 -1
  45. data/lib/jets/resource/sns/topic.rb +1 -1
  46. data/lib/jets/resource/sns/topic_policy.rb +40 -0
  47. data/lib/jets/resource/sqs/queue.rb +1 -1
  48. data/lib/jets/stack.rb +19 -3
  49. data/lib/jets/stack/builder.rb +6 -1
  50. data/lib/jets/stack/depends.rb +36 -0
  51. data/lib/jets/stack/depends/item.rb +9 -0
  52. data/lib/jets/stack/function.rb +19 -10
  53. data/lib/jets/stack/main/dsl.rb +4 -0
  54. data/lib/jets/stack/main/extensions/iam.rb +8 -0
  55. data/lib/jets/stack/main/extensions/lambda.rb +20 -7
  56. data/lib/jets/stack/main/extensions/s3.rb +12 -0
  57. data/lib/jets/stack/main/extensions/sns.rb +4 -0
  58. data/lib/jets/stack/s3_event.rb +87 -0
  59. data/lib/jets/turbine.rb +11 -0
  60. data/lib/jets/version.rb +1 -1
  61. metadata +48 -2
@@ -23,7 +23,7 @@ class Jets::Cfn::Builders
23
23
 
24
24
  def build_minimal_resources
25
25
  # Initial s3 bucket, used to store code zipfile and templates Jets generates
26
- resource = Jets::Resource::S3.new
26
+ resource = Jets::Resource::S3::Bucket.new(logical_id: "s3_bucket")
27
27
  add_resource(resource)
28
28
  add_outputs(resource.outputs)
29
29
 
@@ -170,6 +170,9 @@ module Jets::Commands
170
170
  end
171
171
 
172
172
  # Add internal Jets controllers if they are being used
173
+ # TODO: Interesting, this eventually just used to generate handlers and controllers only.
174
+ # Maybe rename to make that clear.
175
+ # The copying of other internal files like views is done in builders/code_builder.rb copy_internal_jets_code
173
176
  def self.internal_app_files
174
177
  paths = []
175
178
  controllers = File.expand_path("../../internal/app/controllers/jets", __FILE__)
@@ -180,6 +183,9 @@ module Jets::Commands
180
183
  rack_catchall = Jets::Router.has_controller?("Jets::RackController")
181
184
  paths << "#{controllers}/rack_controller.rb" if rack_catchall
182
185
 
186
+ mailer_controller = Jets::Router.has_controller?("Jets::MailersController")
187
+ paths << "#{controllers}/mailers_controller.rb" if mailer_controller
188
+
183
189
  if Jets.config.prewarm.enable
184
190
  jobs = File.expand_path("../../internal/app/jobs/jets", __FILE__)
185
191
  paths << "#{jobs}/preheat_job.rb"
@@ -26,11 +26,21 @@ module Jets::Commands
26
26
 
27
27
  # Build code after the minimal stack because need s3 bucket for assets
28
28
  # on_aws? and s3_base_url logic
29
+ # TODO: possible deploy hook point: before_build
29
30
  build_code
30
31
 
32
+ # TODO: possible deploy hook point: before_ship
33
+ create_s3_event_buckets
31
34
  ship(stack_type: :full, s3_bucket: s3_bucket)
32
35
  end
33
36
 
37
+ def create_s3_event_buckets
38
+ buckets = Jets::Job::Base.s3_events.keys
39
+ buckets.each do |bucket|
40
+ Jets::AwsServices::S3Bucket.ensure_exists(bucket)
41
+ end
42
+ end
43
+
34
44
  def delete_minimal_stack
35
45
  puts "Existing stack is in ROLLBACK_COMPLETE state from a previous failed minimal deploy. Deleting stack and continuing."
36
46
  cfn.delete_stack(stack_name: stack_name)
@@ -1,4 +1,7 @@
1
1
  Jets.application.configure do
2
2
  # Example:
3
3
  # config.function.memory_size = 1536
4
- end
4
+
5
+ # config.action_mailer.raise_delivery_errors = false
6
+ # Docs: http://rubyonjets.com/docs/email-sending/
7
+ end
@@ -1,4 +1,9 @@
1
1
  Jets.application.configure do
2
2
  # Example:
3
3
  # config.function.memory_size = 2048
4
- end
4
+
5
+ # Ignore bad email addresses and do not raise email delivery errors.
6
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
7
+ # Docs: http://rubyonjets.com/docs/email-sending/
8
+ # config.action_mailer.raise_delivery_errors = false
9
+ end
@@ -0,0 +1,7 @@
1
+ Jets.application.configure do
2
+ # Tell Action Mailer not to deliver emails to the real world.
3
+ # The :test delivery method accumulates sent emails in the
4
+ # ActionMailer::Base.deliveries array.
5
+ # Docs: http://rubyonjets.com/docs/email-sending/
6
+ config.action_mailer.delivery_method = :test
7
+ end
@@ -56,8 +56,7 @@ class Jets::Controller
56
56
  def log_info_start
57
57
  display_event = @event.dup
58
58
  display_event['body'] = '[BASE64_ENCODED]' if @event['isBase64Encoded']
59
- # Interesting, JSON.dump makes logging look like JSON.pretty_generate in
60
- # CloudWatch but not locally. This is what we want.
59
+ # JSON.dump makes logging look pretty in CloudWatch logs because it keeps it on 1 line
61
60
  ip = request.ip
62
61
  Jets.logger.info "Started #{@event['httpMethod']} \"#{@event['path']}\" for #{ip} at #{Time.now}"
63
62
  Jets.logger.info "Processing #{self.class.name}##{@meth}"
@@ -30,6 +30,7 @@ module Jets::Controller::Rendering
30
30
  if drop_content_info?(status)
31
31
  body = StringIO.new
32
32
  else
33
+
33
34
  renderer = ActionController::Base.renderer.new(renderer_options)
34
35
  body = renderer.render(render_options)
35
36
  body = StringIO.new(body)
@@ -191,7 +192,7 @@ module Jets::Controller::Rendering
191
192
  require "jets/overrides/rails"
192
193
 
193
194
  # Load helpers
194
- # Assign local variable because scoe in the `:action_view do` changes
195
+ # Assign local variable because scope in the `:action_view do` block changes
195
196
  app_helper_classes = find_app_helper_classes
196
197
  ActiveSupport.on_load :action_view do
197
198
  include ApplicationHelper # include first
@@ -207,11 +208,18 @@ module Jets::Controller::Rendering
207
208
 
208
209
  # Does not include ApplicationHelper, will include ApplicationHelper explicitly first.
209
210
  def find_app_helper_classes
211
+ internal_path = File.expand_path("../../internal", File.dirname(__FILE__))
212
+ internal_classes = find_app_helper_classes_from(internal_path)
213
+ app_classes = find_app_helper_classes_from(Jets.root)
214
+ (internal_classes + app_classes).uniq
215
+ end
216
+
217
+ def find_app_helper_classes_from(project_root)
210
218
  klasses = []
211
- expression = "#{Jets.root}/app/helpers/**/*"
219
+ expression = "#{project_root}/app/helpers/**/*"
212
220
  Dir.glob(expression).each do |path|
213
221
  next unless File.file?(path)
214
- class_name = path.sub("#{Jets.root}/app/helpers/","").sub(/\.rb/,'')
222
+ class_name = path.sub("#{project_root}/app/helpers/","").sub(/\.rb/,'')
215
223
  unless class_name == "application_helper"
216
224
  klasses << class_name.classify.constantize # autoload
217
225
  end
data/lib/jets/core.rb CHANGED
@@ -55,72 +55,6 @@ module Jets::Core
55
55
  Jets::VERSION
56
56
  end
57
57
 
58
- def eager_load!
59
- eager_load_jets
60
- eager_load_app
61
- end
62
-
63
- # Eager load jet's lib and classes
64
- def eager_load_jets
65
- lib_jets = File.expand_path(".", File.dirname(__FILE__))
66
- Dir.glob("#{lib_jets}/**/*.rb").select do |path|
67
- next if !File.file?(path)
68
- next if skip_eager_load_paths?(path)
69
-
70
- path = path.sub("#{lib_jets}/","jets/")
71
- class_name = path
72
- .sub(/\.rb$/,'') # remove .rb
73
- .sub(/^\.\//,'') # remove ./
74
- .sub(/app\/\w+\//,'') # remove app/controllers or app/jobs etc
75
- .camelize
76
- # special class mappings
77
- class_name = class_mappings(class_name)
78
- class_name.constantize # use constantize instead of require so dont have to worry about order.
79
- end
80
- end
81
-
82
- # Skip these paths because eager loading doesnt work for them.
83
- def skip_eager_load_paths?(path)
84
- path =~ %r{/cli} ||
85
- path =~ %r{/core_ext} ||
86
- path =~ %r{/default/application} ||
87
- path =~ %r{/functions} ||
88
- path =~ %r{/internal/app} ||
89
- path =~ %r{/jets/stack} ||
90
- path =~ %r{/overrides} ||
91
- path =~ %r{/rackup_wrappers} ||
92
- path =~ %r{/reconfigure_rails} ||
93
- path =~ %r{/templates/} ||
94
- path =~ %r{/turbo/project/} ||
95
- path =~ %r{/version} ||
96
- path =~ %r{/webpacker} ||
97
- path =~ %r{/jets/spec}
98
- end
99
-
100
- def class_mappings(class_name)
101
- map = {
102
- "Jets::Io" => "Jets::IO",
103
- }
104
- map[class_name] || class_name
105
- end
106
-
107
- # Eager load user's application
108
- def eager_load_app
109
- Dir.glob("#{Jets.root}/app/**/*.rb").select do |path|
110
- next if !File.file?(path) or path =~ %r{/javascript/} or path =~ %r{/views/}
111
- next if path.include?('app/functions') || path.include?('app/shared/functions') || path.include?('app/internal/functions')
112
-
113
- class_name = path
114
- .sub(/\.rb$/,'') # remove .rb
115
- .sub(%{^\./},'') # remove ./
116
- .sub("#{Jets.root}/",'')
117
- .sub(%r{app/shared/\w+/},'') # remove shared/resources or shared/extensions
118
- .sub(%r{app/\w+/},'') # remove app/controllers or app/jobs etc
119
- class_name = class_name.classify
120
- class_name.constantize # use constantize instead of require so dont have to worry about order.
121
- end
122
- end
123
-
124
58
  # NOTE: In development this will always be 1 because the app gets reloaded.
125
59
  # On AWS Lambda, this will be ever increasing until the container gets replaced.
126
60
  @@call_count = 0
@@ -161,18 +95,17 @@ module Jets::Core
161
95
  end
162
96
 
163
97
  def on_exception(exception)
164
- Jets::Turbine.subclasses.each do |subclass|
165
- reporters = subclass.on_exceptions || []
166
- reporters.each do |label, block|
167
- block.call(exception)
168
- end
169
- end
98
+ Jets::Booter.run_turbines(:on_exceptions)
170
99
  end
171
100
 
172
101
  def custom_domain?
173
102
  Jets.config.domain.hosted_zone_name
174
103
  end
175
104
 
105
+ def s3_event?
106
+ !Jets::Job::Base.s3_events.empty?
107
+ end
108
+
176
109
  def process(event, context, handler)
177
110
  if event['_prewarm']
178
111
  Jets.increase_prewarm_count
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jets::MailersController < Jets::Controller::Base # :nodoc:
4
+ before_action :find_preview, only: [:preview]
5
+ before_action :set_locale, only: [:preview]
6
+ # TODO: allow multiple actions
7
+ # before_action :find_preview, :set_locale, only: :preview
8
+
9
+ # TODO: add helper_method support
10
+ # helper_method :part_query, :locale_query
11
+
12
+ # TODO: content_security_policy
13
+ # content_security_policy(false)
14
+
15
+ def index
16
+ @previews = ActionMailer::Preview.all
17
+ @page_title = "Mailer Previews"
18
+ end
19
+
20
+ def preview
21
+ if params[:path] == @preview.preview_name
22
+ @page_title = "Mailer Previews for #{@preview.preview_name}"
23
+ render :mailer
24
+ else
25
+ @email_action = File.basename(params[:path])
26
+
27
+ if @preview.email_exists?(@email_action)
28
+ @email = @preview.call(@email_action, params)
29
+
30
+ if params[:part]
31
+ part_type = Mime::Type.lookup(params[:part])
32
+
33
+ if part = find_part(part_type)
34
+ response.headers["Content-Type"] = params[:part]
35
+ render plain: part.respond_to?(:decoded) ? part.decoded : part
36
+ else
37
+ raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}"
38
+ end
39
+ else
40
+ @part = find_preferred_part(Mime[:html], Mime[:text])
41
+ render :email, layout: false, formats: %w[html]
42
+ end
43
+ else
44
+ raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}"
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+ def show_previews? # :doc:
51
+ ActionMailer::Base.show_previews
52
+ end
53
+
54
+ def find_preview # :doc:
55
+ candidates = []
56
+ params[:path].to_s.scan(%r{/|$}) { candidates << $` } # ` # hack to fix syntax highlighting
57
+ preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) }
58
+
59
+ if preview
60
+ @preview = ActionMailer::Preview.find(preview)
61
+ else
62
+ raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found"
63
+ end
64
+ end
65
+
66
+ def find_preferred_part(*formats) # :doc:
67
+ formats.each do |format|
68
+ if part = @email.find_first_mime_type(format)
69
+ return part
70
+ end
71
+ end
72
+
73
+ if formats.any? { |f| @email.mime_type == f }
74
+ @email
75
+ end
76
+ end
77
+
78
+ def find_part(format) # :doc:
79
+ if part = @email.find_first_mime_type(format)
80
+ part
81
+ elsif @email.mime_type == format
82
+ @email
83
+ end
84
+ end
85
+
86
+ def part_query(mime_type)
87
+ request.query_parameters.merge(part: mime_type).to_query
88
+ end
89
+
90
+ def locale_query(locale)
91
+ request.query_parameters.merge(locale: locale).to_query
92
+ end
93
+
94
+ def set_locale
95
+ I18n.locale = params[:locale] || I18n.default_locale
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ module Jets::MailersHelper
2
+ def part_query(mime_type)
3
+ mime_type
4
+ end
5
+
6
+ def locale_query(locale)
7
+ locale
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ require "active_support/all"
2
+ require "aws-sdk-s3"
3
+ require "cfnresponse"
4
+ include Cfnresponse
5
+
6
+ def lambda_handler(event:, context:)
7
+ # Print out debugging info immediately just in case
8
+ puts "event: #{json_pretty(event)}"
9
+ puts "context: #{json_pretty(context)}"
10
+
11
+ if %w[Create Update].include?(event['RequestType'])
12
+ properties = event["ResourceProperties"].dup
13
+ # After deleting ServiceToken, the rest of the values is the bucket configuration properties.
14
+ properties.delete("ServiceToken")
15
+ configurator = BucketConfigurator.new
16
+ configurator.put(properties)
17
+ end
18
+
19
+ send_response(event, context, "SUCCESS")
20
+
21
+ # We rescue all exceptions and send an message to CloudFormation so we dont have to
22
+ # wait for over an hour for the stack operation to timeout and rollback.
23
+ rescue Exception => e
24
+ puts e.message
25
+ puts e.backtrace
26
+ sleep 10 # a little time for logs to be sent to CloudWatch
27
+ send_response(event, context, "FAILED")
28
+ end
29
+
30
+ ########################################################
31
+
32
+ class BucketConfigurator
33
+ def put(props={})
34
+ # all props including bucket gets passed from the Custom::S3BucketConfiguration resource
35
+ props = props.deep_transform_keys { |k| k.to_s.underscore.to_sym }
36
+ puts "props: #{JSON.dump(props)}"
37
+ s3.put_bucket_notification_configuration(props)
38
+ end
39
+
40
+ def s3
41
+ @s3 ||= Aws::S3::Client.new
42
+ end
43
+ end
@@ -0,0 +1,145 @@
1
+ <!DOCTYPE html>
2
+ <html><head>
3
+ <meta name="viewport" content="width=device-width" />
4
+ <style type="text/css">
5
+ html, body, iframe {
6
+ height: 100%;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ }
12
+
13
+ header {
14
+ width: 100%;
15
+ padding: 10px 0 0 0;
16
+ margin: 0;
17
+ background: white;
18
+ font: 12px "Lucida Grande", sans-serif;
19
+ border-bottom: 1px solid #dedede;
20
+ overflow: hidden;
21
+ }
22
+
23
+ dl {
24
+ margin: 0 0 10px 0;
25
+ padding: 0;
26
+ }
27
+
28
+ dt {
29
+ width: 80px;
30
+ padding: 1px;
31
+ float: left;
32
+ clear: left;
33
+ text-align: right;
34
+ color: #7f7f7f;
35
+ }
36
+
37
+ dd {
38
+ margin-left: 90px; /* 80px + 10px */
39
+ padding: 1px;
40
+ }
41
+
42
+ dd:empty:before {
43
+ content: "\00a0"; // &nbsp;
44
+ }
45
+
46
+ iframe {
47
+ border: 0;
48
+ width: 100%;
49
+ }
50
+ </style>
51
+ </head>
52
+
53
+ <body>
54
+ <header>
55
+ <dl>
56
+ <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
57
+ <dt>SMTP-From:</dt>
58
+ <dd><%= @email.smtp_envelope_from %></dd>
59
+ <% end %>
60
+
61
+ <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
62
+ <dt>SMTP-To:</dt>
63
+ <dd><%= @email.smtp_envelope_to %></dd>
64
+ <% end %>
65
+
66
+ <dt>From:</dt>
67
+ <dd><%= @email.header['from'] %></dd>
68
+
69
+ <% if @email.reply_to %>
70
+ <dt>Reply-To:</dt>
71
+ <dd><%= @email.header['reply-to'] %></dd>
72
+ <% end %>
73
+
74
+ <dt>To:</dt>
75
+ <dd><%= @email.header['to'] %></dd>
76
+
77
+ <% if @email.cc %>
78
+ <dt>CC:</dt>
79
+ <dd><%= @email.header['cc'] %></dd>
80
+ <% end %>
81
+
82
+ <dt>Date:</dt>
83
+ <dd><%= Time.current.rfc2822 %></dd>
84
+
85
+ <dt>Subject:</dt>
86
+ <dd><strong><%= @email.subject %></strong></dd>
87
+
88
+ <% unless @email.attachments.nil? || @email.attachments.empty? %>
89
+ <dt>Attachments:</dt>
90
+ <dd>
91
+ <% @email.attachments.each do |a| %>
92
+ <% filename = a.respond_to?(:original_filename) ? a.original_filename : a.filename %>
93
+ <%= link_to filename, "data:application/octet-stream;charset=utf-8;base64,#{Base64.encode64(a.body.to_s)}", download: filename %>
94
+ <% end %>
95
+ </dd>
96
+ <% end %>
97
+
98
+ <dt>Format:</dt>
99
+ <% if @email.multipart? %>
100
+ <dd>
101
+ <select id="part" onchange="refreshBody(false);">
102
+ <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option>
103
+ <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option>
104
+ </select>
105
+ </dd>
106
+ <% else %>
107
+ <dd id="mime_type" data-mime-type="<%= part_query(@email.mime_type) %>"><%= @email.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd>
108
+ <% end %>
109
+
110
+ <% if I18n.available_locales.count > 1 %>
111
+ <dt>Locale:</dt>
112
+ <dd>
113
+ <select id="locale" onchange="refreshBody(true);">
114
+ <% I18n.available_locales.each do |locale| %>
115
+ <option <%= I18n.locale == locale ? 'selected' : '' %> value="<%= locale_query(locale) %>"><%= locale %></option>
116
+ <% end %>
117
+ </select>
118
+ </dd>
119
+ <% end %>
120
+ </dl>
121
+ </header>
122
+
123
+ <iframe seamless name="messageBody" src="?part=<%= part_query(@part.mime_type) %>"></iframe>
124
+
125
+ <script>
126
+ function refreshBody(reload) {
127
+ var part_select = document.querySelector('select#part');
128
+ var locale_select = document.querySelector('select#locale');
129
+ var iframe = document.getElementsByName('messageBody')[0];
130
+ var part_param = part_select ?
131
+ part_select.options[part_select.selectedIndex].value :
132
+ document.querySelector('#mime_type').dataset.mimeType;
133
+ var fresh_location = '?part=' + part_param;
134
+ var doc = iframe.contentWindow.document;
135
+
136
+ doc.open();
137
+ doc.write("Loading...");
138
+ doc.close();
139
+
140
+ iframe.contentWindow.location = fresh_location;
141
+ }
142
+ </script>
143
+
144
+ </body>
145
+ </html>