1 // movie_root.cpp: The root movie, for Gnash.
2 //
3 // Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012
4 // Free Software Foundation, Inc
5 //
6 // This program 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 // This program 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 this program; if not, write to the Free Software
18 // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 //
20
21 #include "movie_root.h"
22
23 #include <utility>
24 #include <string>
25 #include <sstream>
26 #include <map>
27 #include <bitset>
28 #include <cassert>
29 #include <functional>
30 #include <boost/algorithm/string/replace.hpp>
31 #include <boost/ptr_container/ptr_deque.hpp>
32 #include <boost/algorithm/string/case_conv.hpp>
33 #include <functional>
34
35 #include "GnashSystemIOHeaders.h" // write()
36 #include "log.h"
37 #include "MovieClip.h"
38 #include "Movie.h"
39 #include "VM.h"
40 #include "ExecutableCode.h"
41 #include "URL.h"
42 #include "namedStrings.h"
43 #include "GnashException.h"
44 #include "sound_handler.h"
45 #include "Timers.h"
46 #include "GnashKey.h"
47 #include "GnashAlgorithm.h"
48 #include "GnashNumeric.h"
49 #include "Global_as.h"
50 #include "utf8.h"
51 #include "IOChannel.h"
52 #include "RunResources.h"
53 #include "Renderer.h"
54 #include "ExternalInterface.h"
55 #include "TextField.h"
56 #include "Button.h"
57 #include "Transform.h"
58 #include "StreamProvider.h"
59 #include "SystemClock.h"
60 #include "as_function.h"
61
62 #ifdef USE_SWFTREE
63 # include "tree.hh"
64 #endif
65
66 //#define GNASH_DEBUG 1
67 //#define GNASH_DEBUG_LOADMOVIE_REQUESTS_PROCESSING 1
68 //#define GNASH_DEBUG_TIMERS_EXPIRATION 1
69
70 // Defining the macro below prints info about
71 // cleanup of live chars (advanceable + key/mouse listeners)
72 // Is useful in particular to check for cost of multiple scans
73 // when a movie destruction destrois more elements.
74 //
75 // NOTE: I think the whole confusion here was introduced
76 // by zou making it "optional" to ::unload() childs
77 // when being unloaded. Zou was trying to avoid
78 // queuing an onUnload event, which I suggested we'd
79 // do by having unload() take an additional argument
80 // or similar. Failing to tag childs as unloaded
81 // will result in tagging them later (in ::destroy)
82 // which will require scanning the lists again
83 // (key/mouse + advanceable).
84 // See https://savannah.gnu.org/bugs/index.php?21804
85 //
86 //#define GNASH_DEBUG_DLIST_CLEANUP 1
87
88 namespace gnash {
89
90 // Forward declarations
91 namespace {
92 bool generate_mouse_button_events(movie_root& mr, MouseButtonState& ms);
93 const DisplayObject* getNearestObject(const DisplayObject* o);
94 as_object* getBuiltinObject(movie_root& mr, const ObjectURI& cl);
95 void advanceLiveChar(MovieClip* ch);
96 void notifyLoad(MovieClip* ch);
97 }
98
99 // Utility classes
100 namespace {
101
102 /// Execute an ActiveRelay if the object has that type.
103 struct ExecuteCallback
104 {
operator ()gnash::__anon959a94ef0211::ExecuteCallback105 void operator()(const as_object* o) const {
106 ActiveRelay* a;
107 if (isNativeType(o, a)) {
108 a->update();
109 }
110 }
111 };
112
113 /// Identify and delete ExecutableCode that matches a particular target.
114 class RemoveTargetCode
115 {
116 public:
RemoveTargetCode(DisplayObject * target)117 RemoveTargetCode(DisplayObject* target) : _target(target) {}
operator ()(const ExecutableCode & c) const118 bool operator()(const ExecutableCode& c) const {
119 return _target == c.target();
120 }
121 private:
122 DisplayObject* _target;
123 };
124
125 void
clear(movie_root::ActionQueue & aq)126 clear(movie_root::ActionQueue& aq)
127 {
128 std::for_each(aq.begin(), aq.end(),
129 std::mem_fn(&movie_root::ActionQueue::value_type::clear));
130 }
131
132 } // anonymous namespace
133
134
movie_root(VirtualClock & clock,const RunResources & runResources)135 movie_root::movie_root(VirtualClock& clock, const RunResources& runResources)
136 :
137 _gc(*this),
138 _runResources(runResources),
139 _vm(*this, clock),
140 _interfaceHandler(nullptr),
141 _fsCommandHandler(nullptr),
142 _stageWidth(1),
143 _stageHeight(1),
144 m_background_color(255, 255, 255, 255),
145 m_background_color_set(false),
146 _mouseX(0),
147 _mouseY(0),
148 _lastTimerId(0),
149 _lastKeyEvent(key::INVALID),
150 _currentFocus(nullptr),
151 _movies(),
152 _rootMovie(nullptr),
153 _invalidated(true),
154 _disableScripts(false),
155 _processingActionLevel(PRIORITY_SIZE),
156 _hostfd(-1),
157 _controlfd(-1),
158 _quality(QUALITY_HIGH),
159 _alignMode(0ULL),
160 _allowScriptAccess(SCRIPT_ACCESS_SAME_DOMAIN),
161 _showMenu(true),
162 _scaleMode(SCALEMODE_SHOWALL),
163 _displayState(DISPLAYSTATE_NORMAL),
164 _recursionLimit(), // set in ctor body
165 _timeoutLimit(), // set in ctor body
166 _movieAdvancementDelay(83), // ~12 fps by default
167 _lastMovieAdvancement(0),
168 _unnamedInstance(0),
169 _movieLoader(*this)
170 {
171 // This takes care of informing the renderer (if present) too.
172 setQuality(QUALITY_HIGH);
173
174 gnash::RcInitFile& rcfile = gnash::RcInitFile::getDefaultInstance();
175 _recursionLimit = rcfile.getScriptsRecursionLimit();
176 _timeoutLimit = rcfile.getScriptsTimeout();
177 }
178
179 void
disableScripts()180 movie_root::disableScripts()
181 {
182 _disableScripts = true;
183
184 // NOTE: we won't clear the action queue now
185 // to avoid invalidating iterators as we've
186 // been probably called during processing
187 // of the queue.
188 }
189
~movie_root()190 movie_root::~movie_root()
191 {
192 clear(_actionQueue);
193 _intervalTimers.clear();
194 _movieLoader.clear();
195
196 assert(testInvariant());
197 }
198
199 Movie*
init(movie_definition * def,const MovieClip::MovieVariables & vars)200 movie_root::init(movie_definition* def, const MovieClip::MovieVariables& vars)
201 {
202 _vm.setSWFVersion(def->get_version());
203
204 Movie* mr = def->createMovie(*_vm.getGlobal());
205 mr->setVariables(vars);
206 setRootMovie(mr);
207 return mr;
208 }
209
210 void
setRootMovie(Movie * movie)211 movie_root::setRootMovie(Movie* movie)
212 {
213 _rootMovie = movie;
214
215 const movie_definition* md = movie->definition();
216 float fps = md->get_frame_rate();
217 _movieAdvancementDelay = static_cast<int>(1000/fps);
218
219 _lastMovieAdvancement = _vm.getTime();
220
221 _stageWidth = static_cast<int>(md->get_width_pixels());
222 _stageHeight = static_cast<int>(md->get_height_pixels());
223
224 movie->set_depth(DisplayObject::staticDepthOffset);
225
226 try {
227 setLevel(0, movie);
228
229 // actions in first frame of _level0 must execute now,
230 // before next advance,
231 // or they'll be executed with _currentframe being set to 2
232 processActionQueue();
233 }
234 catch (const ActionLimitException& al) {
235 handleActionLimitHit(al.what());
236 }
237 catch (const ActionParserException& e) {
238 log_error(_("ActionParserException thrown during setRootMovie: %s"),
239 e.what());
240 }
241
242 cleanupAndCollect();
243 }
244
245 bool
queryInterface(const std::string & what) const246 movie_root::queryInterface(const std::string& what) const
247 {
248 bool disable = true;
249 if (_interfaceHandler) {
250 disable = callInterface<bool>(HostMessage(HostMessage::QUERY, what));
251 }
252 else {
253 log_error(_("No user interface registered, assuming 'Yes' answer to question: %s"), what);
254 }
255 return disable;
256 }
257
258 void
setStreamBlock(int id,int block)259 movie_root::setStreamBlock(int id, int block)
260 {
261 if (!_timelineSound) {
262 _timelineSound = SoundStream(id, block);
263 return;
264 }
265
266 // Don't replace timeline stream.
267 if (_timelineSound->id != id) return;
268
269 _timelineSound->block = block;
270 }
271
272 void
stopStream(int id)273 movie_root::stopStream(int id)
274 {
275 if (!_timelineSound) return;
276 if (_timelineSound->id == id) _timelineSound.reset();
277 }
278
279 void
registerClass(const SWF::DefinitionTag * sprite,as_function * cls)280 movie_root::registerClass(const SWF::DefinitionTag* sprite, as_function* cls)
281 {
282 _registeredClasses[sprite] = cls;
283 }
284
285 as_function*
getRegisteredClass(const SWF::DefinitionTag * sprite) const286 movie_root::getRegisteredClass(const SWF::DefinitionTag* sprite) const
287 {
288 RegisteredClasses::const_iterator it = _registeredClasses.find(sprite);
289 if (it == _registeredClasses.end()) return nullptr;
290 return it->second;
291 }
292
293 void
handleActionLimitHit(const std::string & msg)294 movie_root::handleActionLimitHit(const std::string& msg)
295 {
296 log_debug("Disabling scripts: %1%", msg);
297 disableScripts();
298 clear(_actionQueue);
299 }
300
301 void
cleanupAndCollect()302 movie_root::cleanupAndCollect()
303 {
304 // Cleanup the stack.
305 _vm.getStack().clear();
306
307 // Reset the constant pool
308 _vm.setConstantPool(nullptr);
309
310 cleanupDisplayList();
311 _gc.fuzzyCollect();
312 }
313
314 /* private */
315 void
setLevel(unsigned int num,Movie * movie)316 movie_root::setLevel(unsigned int num, Movie* movie)
317 {
318 assert(movie != nullptr);
319 assert(static_cast<unsigned int>(movie->get_depth()) ==
320 num + DisplayObject::staticDepthOffset);
321
322
323 Levels::iterator it = _movies.find(movie->get_depth());
324 if (it == _movies.end()) {
325 _movies[movie->get_depth()] = movie;
326 }
327 else {
328 // don't leak overloaded levels
329
330 MovieClip* lm = it->second;
331 if (lm == _rootMovie) {
332 // NOTE: this is not enough to trigger
333 // an application reset. Was tested
334 // but not automated. If curious
335 // use swapDepths against _level0
336 // and load into the new target while
337 // a timeout/interval is active.
338 log_debug("Replacing starting movie");
339 }
340
341 if (num == 0) {
342
343 log_debug("Loading into _level0");
344
345 // NOTE: this was tested but not automated, the
346 // test sets an interval and then loads something
347 // in _level0. The result is the interval is disabled.
348 _intervalTimers.clear();
349
350 // TODO: check what else we should do in these cases
351 // (like, unregistering all childs etc...)
352 // Tested, but not automated, is that other
353 // levels should be maintained alive.
354 // Sat Nov 14 10:31:19 CET 2009
355 // ^^^ not confirmed in this date, I think other levels
356 // are dropped too! (strk)
357
358 _stageWidth = movie->widthPixels();
359 _stageHeight = movie->heightPixels();
360
361 // notify stage replacement
362 if (_interfaceHandler) {
363 const HostMessage e(HostMessage::RESIZE_STAGE,
364 std::make_pair(_stageWidth, _stageHeight));
365 _interfaceHandler->call(e);
366 }
367 }
368
369 it->second->destroy();
370 it->second = movie;
371 }
372
373 movie->set_invalidated();
374
375 /// Notify placement
376 movie->construct();
377
378 assert(testInvariant());
379 }
380
381 void
swapLevels(MovieClip * movie,int depth)382 movie_root::swapLevels(MovieClip* movie, int depth)
383 {
384 assert(movie);
385
386 //#define GNASH_DEBUG_LEVELS_SWAPPING 1
387
388 const int oldDepth = movie->get_depth();
389
390 #ifdef GNASH_DEBUG_LEVELS_SWAPPING
391 log_debug("Before swapLevels (source depth %d, target depth %d) levels are: ",
392 oldDepth, depth);
393 for (Levels::const_iterator i=_movies.begin(), e=_movies.end(); i!=e; ++i) {
394 log_debug(" %d: %p (%s @ depth %d)", i->first,
395 (void*)(i->second), i->second->getTarget(),
396 i->second->get_depth());
397 }
398 #endif
399 // should include _level0 !
400 if (oldDepth < DisplayObject::staticDepthOffset) {
401 IF_VERBOSE_ASCODING_ERRORS(
402 log_aserror(_("%s.swapDepth(%d): movie has a depth (%d) below "
403 "static depth zone (%d), won't swap its depth"),
404 movie->getTarget(), depth, oldDepth,
405 DisplayObject::staticDepthOffset);
406 );
407 return;
408 }
409
410 if (oldDepth >= 0) {
411 IF_VERBOSE_ASCODING_ERRORS(
412 log_aserror(_("%s.swapDepth(%d): movie has a depth (%d) below "
413 "static depth zone (%d), won't swap its depth"),
414 movie->getTarget(), depth, oldDepth,
415 DisplayObject::staticDepthOffset);
416 );
417 return;
418 }
419
420 const int oldNum = oldDepth;
421 Levels::iterator oldIt = _movies.find(oldNum);
422 if (oldIt == _movies.end()) {
423 log_debug("%s.swapDepth(%d): target depth (%d) contains no movie",
424 movie->getTarget(), depth, oldNum);
425 return;
426 }
427
428 const int newNum = depth;
429 movie->set_depth(depth);
430 Levels::iterator targetIt = _movies.find(newNum);
431 if (targetIt == _movies.end()) {
432 _movies.erase(oldIt);
433 _movies[newNum] = movie;
434 }
435 else {
436 MovieClip* otherMovie = targetIt->second;
437 otherMovie->set_depth(oldDepth);
438 oldIt->second = otherMovie;
439 targetIt->second = movie;
440 }
441
442 #ifdef GNASH_DEBUG_LEVELS_SWAPPING
443 log_debug("After swapLevels levels are: ");
444 for (Levels::const_iterator i=_movies.begin(), e=_movies.end(); i!=e; ++i) {
445 log_debug(" %d: %p (%s @ depth %d)", i->first,
446 (void*)(i->second), i->second->getTarget(),
447 i->second->get_depth());
448 }
449 #endif
450
451 // TODO: invalidate self, not the movie
452 // movie_root::setInvalidated() seems
453 // to do just that, if anyone feels
454 // like more closely research on this
455 // (does level swapping require full redraw always?)
456 movie->set_invalidated();
457
458 assert(testInvariant());
459 }
460
461 void
dropLevel(int depth)462 movie_root::dropLevel(int depth)
463 {
464 // should be checked by caller
465 // TODO: don't use a magic number! See MovieClip::removeMovieClip().
466 assert(depth >= 0 && depth <= 1048575);
467
468 Levels::iterator it = _movies.find(depth);
469 if (it == _movies.end()) {
470 log_error(_("movie_root::dropLevel called against a movie not found in the levels container"));
471 return;
472 }
473
474 MovieClip* mo = it->second;
475 if (mo == _rootMovie) {
476 IF_VERBOSE_ASCODING_ERRORS(
477 log_aserror(_("Original root movie can't be removed"));
478 );
479 return;
480 }
481
482 // TOCHECK: safe to erase here ?
483
484 // Ignoring return value of unload(), because the only way to handle
485 // an unload failure is to call destroy, which is done anyway.
486 (void)mo->unload();
487 mo->destroy();
488 _movies.erase(it);
489
490 assert(testInvariant());
491 }
492
493 void
replaceLevel(unsigned int num,Movie * extern_movie)494 movie_root::replaceLevel(unsigned int num, Movie* extern_movie)
495 {
496 extern_movie->set_depth(num + DisplayObject::staticDepthOffset);
497 Levels::iterator it = _movies.find(extern_movie->get_depth());
498 if (it == _movies.end()) {
499 log_error(_("TESTME: loadMovie called on level %d which is not available at load time, skipped placement for now"));
500 return;
501 }
502
503 // TODO: rework this to avoid the double scan
504 setLevel(num, extern_movie);
505 }
506
507 MovieClip*
getLevel(unsigned int num) const508 movie_root::getLevel(unsigned int num) const
509 {
510 Levels::const_iterator i =
511 _movies.find(num + DisplayObject::staticDepthOffset);
512
513 if (i == _movies.end()) return nullptr;
514
515 return i->second;
516 }
517
518 void
reset()519 movie_root::reset()
520 {
521 sound::sound_handler* sh = _runResources.soundHandler();
522 if (sh) sh->reset();
523
524 // reset background color, to allow
525 // next load to set it again.
526 m_background_color = rgba(255, 255, 255, 255);
527 m_background_color_set = false;
528
529 // wipe out live chars
530 _liveChars.clear();
531
532 // wipe out queued actions
533 clear(_actionQueue);
534
535 // wipe out all levels
536 _movies.clear();
537
538 // remove all intervals
539 _intervalTimers.clear();
540
541 // remove all loadMovie requests
542 _movieLoader.clear();
543
544 // Remove button key events.
545 _buttonListeners.clear();
546
547 // Cleanup the stack.
548 _vm.getStack().clear();
549
550 // Run the garbage collector again
551 _gc.fuzzyCollect();
552
553 setInvalidated();
554
555 _disableScripts = false;
556
557 _timelineSound.reset();
558 }
559
560 void
setDimensions(size_t w,size_t h)561 movie_root::setDimensions(size_t w, size_t h)
562 {
563 assert(testInvariant());
564
565 _stageWidth = w;
566 _stageHeight = h;
567
568 if (_scaleMode == SCALEMODE_NOSCALE) {
569 as_object* stage = getBuiltinObject(*this,
570 getURI(_vm, NSV::CLASS_STAGE));
571 if (stage) {
572 callMethod(stage, getURI(_vm, NSV::PROP_BROADCAST_MESSAGE),
573 "onResize");
574 }
575
576 }
577
578 assert(testInvariant());
579 }
580
581 bool
mouseMoved(std::int32_t x,std::int32_t y)582 movie_root::mouseMoved(std::int32_t x, std::int32_t y)
583 {
584 assert(testInvariant());
585
586 _mouseX = x;
587 _mouseY = y;
588 return notify_mouse_listeners(event_id(event_id::MOUSE_MOVE));
589 }
590
591
592 bool
keyEvent(key::code k,bool down)593 movie_root::keyEvent(key::code k, bool down)
594 {
595 _lastKeyEvent = k;
596 const size_t keycode = key::codeMap[k][key::KEY];
597 if (keycode < key::KEYCOUNT) {
598 _unreleasedKeys.set(keycode, down);
599 }
600
601 LiveChars copy = _liveChars;
602 for (MovieClip* const ch : copy) {
603
604 if (ch->unloaded()) continue;
605
606 if (down) {
607 ch->notifyEvent(event_id(event_id::KEY_DOWN, key::INVALID));
608 ch->notifyEvent(event_id(event_id::KEY_PRESS, k));
609 }
610 else {
611 ch->notifyEvent(event_id(event_id::KEY_UP, key::INVALID));
612 }
613 }
614
615 // Broadcast event to Key._listeners.
616 as_object* key = getBuiltinObject(*this, getURI(_vm, NSV::CLASS_KEY));
617 if (key) {
618
619 try {
620 // Can throw an action limit exception if the stack limit is 0 or 1,
621 // i.e. if the stack is at the limit before it contains anything.
622 // A stack limit like that is hardly of any use, but could be used
623 // maliciously to crash Gnash.
624 callMethod(key, getURI(_vm, NSV::PROP_BROADCAST_MESSAGE),
625 down ? "onKeyDown" : "onKeyUp");
626 }
627 catch (const ActionLimitException &e) {
628 log_error(_("ActionLimits hit notifying key listeners: %s."),
629 e.what());
630 clear(_actionQueue);
631 }
632 }
633
634 if (down) {
635
636 // NB: Button handling is not correct, as only one button should
637 // respond to any key. A test is in misc-ming.all/KeyEventOrder.c.
638 // However, the previous attempt to fix caused real-life failures:
639 // see bug #33889.
640 ButtonListeners copy = _buttonListeners;
641 for (Button* button : copy) {
642 if (!button->unloaded()) {
643 button->keyPress(k);
644 }
645 }
646
647 // If we're focused on an editable text field, finally the text
648 // is updated
649 TextField* tf = dynamic_cast<TextField*>(_currentFocus);
650 if (tf) tf->keyInput(k);
651 }
652
653 processActionQueue();
654
655 return false;
656 }
657
658 bool
mouseWheel(int delta)659 movie_root::mouseWheel(int delta)
660 {
661 as_object* mouseObj =
662 getBuiltinObject(*this, getURI(_vm, NSV::CLASS_MOUSE));
663 if (!mouseObj) return false;
664
665 const std::int32_t x = pixelsToTwips(_mouseX);
666 const std::int32_t y = pixelsToTwips(_mouseY);
667
668 DisplayObject* i = getTopmostMouseEntity(x, y);
669
670 // Always called with two arguments.
671 callMethod(mouseObj, getURI(_vm,NSV::PROP_BROADCAST_MESSAGE), "onMouseWheel",
672 delta, i ? getObject(i) : as_value());
673
674 return true;
675 }
676
677 bool
mouseClick(bool mouse_pressed)678 movie_root::mouseClick(bool mouse_pressed)
679 {
680 assert(testInvariant());
681
682 _mouseButtonState.isDown = mouse_pressed;
683
684 if (mouse_pressed) {
685 return notify_mouse_listeners(event_id(event_id::MOUSE_DOWN));
686 }
687 return notify_mouse_listeners(event_id(event_id::MOUSE_UP));
688 }
689
690
691 bool
fire_mouse_event()692 movie_root::fire_mouse_event()
693 {
694 assert(testInvariant());
695
696 std::int32_t x = pixelsToTwips(_mouseX);
697 std::int32_t y = pixelsToTwips(_mouseY);
698
699 // Generate a mouse event
700 _mouseButtonState.topmostEntity = getTopmostMouseEntity(x, y);
701
702 // Set _droptarget if dragging a sprite
703 DisplayObject* draggingChar = getDraggingCharacter();
704 if (draggingChar) {
705 MovieClip* dragging = draggingChar->to_movie();
706 if (dragging) {
707 // TODO: optimize making findDropTarget and getTopmostMouseEntity
708 // use a single scan.
709 const DisplayObject* dropChar = findDropTarget(x, y, dragging);
710 if (dropChar) {
711 // Use target of closest script DisplayObject containing this
712 dropChar = getNearestObject(dropChar);
713 dragging->setDropTarget(dropChar->getTargetPath());
714 }
715 else dragging->setDropTarget("");
716 }
717 }
718
719 bool need_redraw = false;
720
721 // FIXME: need_redraw might also depend on actual
722 // actions execution (consider updateAfterEvent).
723
724 try {
725 need_redraw = generate_mouse_button_events(*this, _mouseButtonState);
726 processActionQueue();
727 }
728 catch (const ActionLimitException& al) {
729 handleActionLimitHit(al.what());
730 }
731
732 return need_redraw;
733 }
734
735 std::pair<std::int32_t, std::int32_t>
mousePosition() const736 movie_root::mousePosition() const
737 {
738 assert(testInvariant());
739 return std::make_pair(_mouseX, _mouseY);
740 }
741
742 void
setDragState(const DragState & st)743 movie_root::setDragState(const DragState& st)
744 {
745 _dragState = st;
746
747 DisplayObject* ch = _dragState->getCharacter();
748
749 if (ch && !_dragState->isLockCentered()) {
750 // Get coordinates of the DisplayObject's origin
751 point origin(0, 0);
752 SWFMatrix chmat = getWorldMatrix(*ch);
753 point world_origin;
754 chmat.transform(&world_origin, origin);
755
756 // Get current mouse coordinates
757 const point world_mouse(pixelsToTwips(_mouseX), pixelsToTwips(_mouseY));
758
759 std::int32_t xoffset = world_mouse.x - world_origin.x;
760 std::int32_t yoffset = world_mouse.y - world_origin.y;
761
762 _dragState->setOffset(xoffset, yoffset);
763 }
764 assert(testInvariant());
765 }
766
767 void
doMouseDrag()768 movie_root::doMouseDrag()
769 {
770 DisplayObject* dragChar = getDraggingCharacter();
771 if (!dragChar) return; // nothing to do
772
773 if (dragChar->unloaded()) {
774 // Reset drag state if dragging char was unloaded
775 _dragState.reset();
776 return;
777 }
778
779 point world_mouse(pixelsToTwips(_mouseX), pixelsToTwips(_mouseY));
780
781 SWFMatrix parent_world_mat;
782 DisplayObject* p = dragChar->parent();
783 if (p) {
784 parent_world_mat = getWorldMatrix(*p);
785 }
786
787 if (!_dragState->isLockCentered()) {
788 world_mouse.x -= _dragState->xOffset();
789 world_mouse.y -= _dragState->yOffset();
790 }
791
792 if (_dragState->hasBounds()) {
793 SWFRect bounds;
794 // bounds are in local coordinate space
795 bounds.enclose_transformed_rect(parent_world_mat,
796 _dragState->getBounds());
797 // Clamp mouse coords within a defined SWFRect.
798 bounds.clamp(world_mouse);
799 }
800
801 parent_world_mat.invert().transform(world_mouse);
802 // Place our origin so that it coincides with the mouse coords
803 // in our parent frame.
804 // TODO: add a DisplayObject::set_translation ?
805 SWFMatrix local = getMatrix(*dragChar);
806 local.set_translation(world_mouse.x, world_mouse.y);
807
808 // no need to update caches when only changing translation
809 dragChar->setMatrix(local);
810 }
811
812 std::uint32_t
addIntervalTimer(std::unique_ptr<Timer> timer)813 movie_root::addIntervalTimer(std::unique_ptr<Timer> timer)
814 {
815 assert(timer.get());
816 assert(testInvariant());
817
818 const size_t id = ++_lastTimerId;
819
820 assert(_intervalTimers.find(id) == _intervalTimers.end());
821
822 _intervalTimers.insert(std::make_pair(id, std::move(timer)));
823
824 return id;
825 }
826
827 bool
clearIntervalTimer(std::uint32_t x)828 movie_root::clearIntervalTimer(std::uint32_t x)
829 {
830 TimerMap::iterator it = _intervalTimers.find(x);
831 if (it == _intervalTimers.end()) {
832 return false;
833 }
834
835 // We do not remove the element here because
836 // we might have been called during execution
837 // of another timer, thus during a scan of the _intervalTimers
838 // container. If we use erase() here, the iterators in executeTimers
839 // would be invalidated. Rather, executeTimers() would check container
840 // elements for being still active and remove the cleared one in a safe way
841 // at each iteration.
842 it->second->clearInterval();
843
844 return true;
845 }
846
847 bool
advance()848 movie_root::advance()
849 {
850 // We can't actually rely on now being later than _lastMovieAdvancement,
851 // so we will have to check. Otherwise we risk elapsed being
852 // contructed from a negative value.
853 const size_t now = std::max<size_t>(_vm.getTime(), _lastMovieAdvancement);
854
855 bool advanced = false;
856
857 try {
858
859 #ifdef USE_SOUND
860 sound::sound_handler* s = _runResources.soundHandler();
861
862 if (s && _timelineSound) {
863
864 if (!s->streamingSound()) {
865 log_error(_("movie_root tracking a streaming sound, but the sound handler is not streaming!"));
866
867 // Give up; we've probably failed to catch up.
868 _timelineSound.reset();
869 } else {
870
871 // -1 for bad result, 0 for first block.
872 // Get the stream block we are currently at.
873 int block = s->getStreamBlock(_timelineSound->id);
874
875 const int startBlock = _timelineSound->block;
876
877 const size_t maxTime = getTimeoutLimit() * 1000;
878 SystemClock clock;
879
880 // If we're behind, we should skip; if we're ahead
881 // (_timelineSound->block > block) we should not advance,
882 // if we're ahead, skip.
883 while (block != -1 && block > _timelineSound->block) {
884 advanced = true;
885 advanceMovie();
886
887 // Movie advance can cause streaming sound to be reset or,
888 // if a MovieClip loops, the current timeline sound block
889 // to be moved earlier. In the latter case we break to
890 // avoid catching up to the old sound position.
891 if (!_timelineSound || _timelineSound->block < startBlock) {
892 break;
893 }
894
895 if (clock.elapsed() > maxTime) {
896 boost::format fmt =
897 boost::format(_("Time exceeded (%1% secs) while "
898 "attempting to catch up to streaming "
899 "sound. Give up on synchronization?"))
900 % maxTime;
901
902 // We'll start synchronizing again anyway when the
903 // next stream block arrives, but this will at least
904 // unblock the user interface.
905 if (queryInterface(fmt.str())) {
906 _timelineSound.reset();
907 break;
908 }
909 }
910
911 // Note: advancing the current sound block here makes
912 // it possible that Gnash will never catch up, if e.g.
913 // executing ActionScript causes the frame rate to drop
914 // even further. Not advancing the sound block means
915 // that Gnash will always catch up to the point we
916 // stored at the start of the loop, but then the audio
917 // stream may restart if it has finished before Gnash
918 // reaches the new position.
919 block = s->getStreamBlock(_timelineSound->id);
920
921 }
922 if (advanced) {
923 _lastMovieAdvancement = now;
924 }
925 }
926 }
927 else {
928 #endif // USE_SOUND
929 // Driven by frame rate
930 const size_t elapsed = now - _lastMovieAdvancement;
931 if (elapsed >= _movieAdvancementDelay) {
932 advanced = true;
933 advanceMovie();
934 _lastMovieAdvancement = now;
935 }
936 #ifdef USE_SOUND
937 }
938 #endif // USE_SOUND
939
940 // Always do this.
941 executeAdvanceCallbacks();
942 executeTimers();
943
944 }
945 catch (const ActionLimitException& al) {
946 // The PP does not disable scripts when the stack limit is reached,
947 // but rather struggles on.
948 // TODO: find a test case for this, if confirmed fix accordingly
949 handleActionLimitHit(al.what());
950 }
951 catch (const ActionParserException& e) {
952 log_error(_("Buffer overread during advance: %s"), e.what());
953 clear(_actionQueue);
954 }
955
956 return advanced;
957 }
958
959 void
advanceMovie()960 movie_root::advanceMovie()
961 {
962 // Do mouse drag, if needed
963 doMouseDrag();
964
965 // Advance all non-unloaded DisplayObjects in the LiveChars list
966 // in reverse order (last added, first advanced)
967 // NOTE: can throw ActionLimitException
968 advanceLiveChars();
969
970 // Process loadMovie requests
971 //
972 // NOTE: should be done before executing timers,
973 // see swfdec's test/trace/loadmovie-case-{5,6}.swf
974 // NOTE: processing loadMovie requests after advanceLiveChars
975 // is known to fix more tests in misc-mtasc.all/levels.swf
976 // to be checked if it keeps the swfdec testsuite safe
977 //
978 _movieLoader.processCompletedRequests();
979
980 // Process queued actions
981 // NOTE: can throw ActionLimitException
982 processActionQueue();
983
984 cleanupAndCollect();
985
986 assert(testInvariant());
987 }
988
989 int
timeToNextFrame() const990 movie_root::timeToNextFrame() const
991 {
992 unsigned int now = _vm.getTime();
993 const int elapsed = now - _lastMovieAdvancement;
994 return _movieAdvancementDelay - elapsed;
995 }
996
997 void
display()998 movie_root::display()
999 {
1000 // GNASH_REPORT_FUNCTION;
1001
1002 assert(testInvariant());
1003
1004 clearInvalidated();
1005
1006 // TODO: should we consider the union of all levels bounds ?
1007 const SWFRect& frame_size = _rootMovie->get_frame_size();
1008 if ( frame_size.is_null() )
1009 {
1010 // TODO: check what we should do if other levels
1011 // have valid bounds
1012 log_debug("original root movie had null bounds, not displaying");
1013 return;
1014 }
1015
1016 Renderer* renderer = _runResources.renderer();
1017 if (!renderer) return;
1018
1019 Renderer::External ex(*renderer, m_background_color,
1020 _stageWidth, _stageHeight,
1021 frame_size.get_x_min(), frame_size.get_x_max(),
1022 frame_size.get_y_min(), frame_size.get_y_max());
1023
1024 for (auto& elem : _movies) {
1025 MovieClip* movie = elem.second;
1026
1027 movie->clear_invalidated();
1028
1029 if (movie->visible() == false) continue;
1030
1031 // null frame size ? don't display !
1032 const SWFRect& sub_frame_size = movie->get_frame_size();
1033
1034 if (sub_frame_size.is_null()) {
1035 log_debug("_level%u has null frame size, skipping", elem.first);
1036 continue;
1037 }
1038
1039 movie->display(*renderer, Transform());
1040 }
1041 }
1042
1043 bool
notify_mouse_listeners(const event_id & event)1044 movie_root::notify_mouse_listeners(const event_id& event)
1045 {
1046 LiveChars copy = _liveChars;
1047 for (MovieClip* const ch : copy)
1048 {
1049 if (!ch->unloaded()) {
1050 ch->mouseEvent(event);
1051 }
1052 }
1053
1054 const ObjectURI& propMouse = getURI(_vm, NSV::CLASS_MOUSE);
1055 const ObjectURI& propBroadcastMessage =
1056 getURI(_vm, NSV::PROP_BROADCAST_MESSAGE);
1057
1058 as_object* mouseObj = getBuiltinObject(*this, propMouse);
1059 if (mouseObj) {
1060
1061 // Can throw an action limit exception if the stack limit is 0 or 1.
1062 // A stack limit like that is hardly of any use, but could be used
1063 // maliciously to crash Gnash.
1064 try {
1065 callMethod(mouseObj, propBroadcastMessage, event.functionName());
1066 }
1067 catch (const ActionLimitException& e) {
1068 log_error(_("ActionLimits hit notifying mouse events: %s."),
1069 e.what());
1070 clear(_actionQueue);
1071 }
1072 }
1073
1074 assert(testInvariant());
1075
1076 if (!copy.empty()) {
1077 // process actions queued in the above step
1078 processActionQueue();
1079 }
1080 return fire_mouse_event();
1081 }
1082
1083 DisplayObject*
getFocus()1084 movie_root::getFocus()
1085 {
1086 return _currentFocus;
1087 }
1088
1089 bool
setFocus(DisplayObject * to)1090 movie_root::setFocus(DisplayObject* to)
1091 {
1092 // Nothing to do if current focus is the same as the new focus.
1093 // _level0 also seems unable to receive focus under any circumstances
1094 // TODO: what about _level1 etc ?
1095 if (to == _currentFocus || to == _rootMovie) {
1096 return false;
1097 }
1098
1099 if (to && !to->handleFocus()) {
1100 // TODO: not clear whether to remove focus in this case.
1101 return false;
1102 }
1103
1104 // Undefined or NULL DisplayObject removes current focus. Otherwise, try
1105 // setting focus to the new DisplayObject. If it fails, remove current
1106 // focus anyway.
1107
1108 // Store previous focus, as the focus needs to change before onSetFocus
1109 // is called and listeners are notified.
1110 DisplayObject* from = _currentFocus;
1111
1112 if (from) {
1113 // Perform any actions required on killing focus (only TextField).
1114 from->killFocus();
1115
1116 /// A valid focus must have an associated object.
1117 assert(getObject(from));
1118 callMethod(getObject(from), NSV::PROP_ON_KILL_FOCUS, getObject(to));
1119 }
1120
1121 _currentFocus = to;
1122
1123 if (to) {
1124 assert(getObject(to));
1125 callMethod(getObject(to), NSV::PROP_ON_SET_FOCUS, getObject(from));
1126 }
1127
1128 as_object* sel = getBuiltinObject(*this, NSV::CLASS_SELECTION);
1129
1130 // Notify Selection listeners with previous and new focus as arguments.
1131 // Either argument may be null.
1132 if (sel) {
1133 callMethod(sel, NSV::PROP_BROADCAST_MESSAGE, "onSetFocus",
1134 getObject(from), getObject(to));
1135 }
1136
1137 assert(testInvariant());
1138
1139 return true;
1140 }
1141
1142 DisplayObject*
getActiveEntityUnderPointer() const1143 movie_root::getActiveEntityUnderPointer() const
1144 {
1145 return _mouseButtonState.activeEntity;
1146 }
1147
1148 DisplayObject*
getDraggingCharacter() const1149 movie_root::getDraggingCharacter() const
1150 {
1151 return _dragState ? _dragState->getCharacter() : nullptr;
1152 }
1153
1154 const DisplayObject*
getEntityUnderPointer() const1155 movie_root::getEntityUnderPointer() const
1156 {
1157 const std::int32_t x = pixelsToTwips(_mouseX);
1158 const std::int32_t y = pixelsToTwips(_mouseY);
1159 return findDropTarget(x, y, getDraggingCharacter());
1160 }
1161
1162
1163 void
setQuality(Quality q)1164 movie_root::setQuality(Quality q)
1165 {
1166 gnash::RcInitFile& rcfile = gnash::RcInitFile::getDefaultInstance();
1167
1168 /// Overridden quality if not negative.
1169 if (rcfile.qualityLevel() >= 0) {
1170 int ql = rcfile.qualityLevel();
1171 ql = std::min<int>(ql, QUALITY_BEST);
1172 q = static_cast<Quality>(ql);
1173 }
1174
1175 if ( _quality != q )
1176 {
1177 // Force a redraw if quality changes
1178 //
1179 // redraw should only happen on next
1180 // frame advancement (tested)
1181 //
1182 setInvalidated();
1183
1184 _quality = q;
1185 }
1186
1187 // We always tell the renderer, because it could
1188 // be the first time we do
1189 Renderer* renderer = _runResources.renderer();
1190 if (renderer) renderer->setQuality(_quality);
1191
1192 }
1193
1194 /// Get actionscript width of stage, in pixels. The width
1195 /// returned depends on the scale mode.
1196 size_t
getStageWidth() const1197 movie_root::getStageWidth() const
1198 {
1199 if (_scaleMode == SCALEMODE_NOSCALE) {
1200 return _stageWidth;
1201 }
1202
1203 // If scaling is allowed, always return the original movie size.
1204 if (_rootMovie) {
1205 return static_cast<size_t>(_rootMovie->widthPixels());
1206 }
1207 return 0;
1208 }
1209
1210 /// Get actionscript height of stage, in pixels. The height
1211 /// returned depends on the scale mode.
1212 size_t
getStageHeight() const1213 movie_root::getStageHeight() const
1214 {
1215 if (_scaleMode == SCALEMODE_NOSCALE) {
1216 return _stageHeight;
1217 }
1218
1219 // If scaling is allowed, always return the original movie size.
1220 if (_rootMovie) {
1221 return static_cast<size_t>(_rootMovie->heightPixels());
1222 }
1223 return 0;
1224 }
1225
1226 /// Takes a short int bitfield: the four bits correspond
1227 /// to the AlignMode enum
1228 void
setStageAlignment(short s)1229 movie_root::setStageAlignment(short s)
1230 {
1231 _alignMode = s;
1232 callInterface(HostMessage(HostMessage::UPDATE_STAGE));
1233 }
1234
1235 /// The mode is one of never, always, with sameDomain the default
1236 void
setAllowScriptAccess(AllowScriptAccessMode mode)1237 movie_root::setAllowScriptAccess(AllowScriptAccessMode mode)
1238 {
1239 _allowScriptAccess = mode;
1240 }
1241
1242 movie_root::AllowScriptAccessMode
getAllowScriptAccess()1243 movie_root::getAllowScriptAccess()
1244 {
1245 return _allowScriptAccess;
1246 }
1247
1248 /// Returns a pair of enum values giving the actual alignment
1249 /// of the stage after align mode flags are evaluated.
1250 movie_root::StageAlign
getStageAlignment() const1251 movie_root::getStageAlignment() const
1252 {
1253 /// L takes precedence over R. Default is centred.
1254 StageHorizontalAlign ha = STAGE_H_ALIGN_C;
1255 if (_alignMode.test(STAGE_ALIGN_L)) ha = STAGE_H_ALIGN_L;
1256 else if (_alignMode.test(STAGE_ALIGN_R)) ha = STAGE_H_ALIGN_R;
1257
1258 /// T takes precedence over B. Default is centred.
1259 StageVerticalAlign va = STAGE_V_ALIGN_C;
1260 if (_alignMode.test(STAGE_ALIGN_T)) va = STAGE_V_ALIGN_T;
1261 else if (_alignMode.test(STAGE_ALIGN_B)) va = STAGE_V_ALIGN_B;
1262
1263 return std::make_pair(ha, va);
1264 }
1265
1266 /// Returns a string that represents the boolean state of the _showMenu
1267 /// variable
1268 bool
getShowMenuState() const1269 movie_root::getShowMenuState() const
1270 {
1271 return _showMenu;
1272 }
1273
1274 /// Sets the value of _showMenu and calls the gui handler to process the
1275 /// fscommand to change the display of the context menu
1276 void
setShowMenuState(bool state)1277 movie_root::setShowMenuState(bool state)
1278 {
1279 _showMenu = state;
1280 //FIXME: The gui code for show menu is semantically different than what
1281 // ActionScript expects it to be. In gtk.cpp the showMenu function hides
1282 // or shows the menubar. Flash expects this option to disable some
1283 // context menu items.
1284 // callInterface is the proper handler for this
1285 callInterface(HostMessage(HostMessage::SHOW_MENU, _showMenu));
1286 }
1287
1288 /// Returns the string representation of the current align mode,
1289 /// which must always be in the order: LTRB
1290 std::string
getStageAlignMode() const1291 movie_root::getStageAlignMode() const
1292 {
1293 std::string align;
1294 if (_alignMode.test(STAGE_ALIGN_L)) align.push_back('L');
1295 if (_alignMode.test(STAGE_ALIGN_T)) align.push_back('T');
1296 if (_alignMode.test(STAGE_ALIGN_R)) align.push_back('R');
1297 if (_alignMode.test(STAGE_ALIGN_B)) align.push_back('B');
1298
1299 return align;
1300 }
1301
1302 void
setStageScaleMode(ScaleMode sm)1303 movie_root::setStageScaleMode(ScaleMode sm)
1304 {
1305 if (_scaleMode == sm) return; // nothing to do
1306
1307 bool notifyResize = false;
1308
1309 // If we go from or to noScale, we notify a resize
1310 // if and only if display viewport is != then actual
1311 // movie size. If there is not yet a _rootMovie (when scaleMode
1312 // is passed as a parameter to the player), we also don't notify a
1313 // resize.
1314 if (_rootMovie &&
1315 (sm == SCALEMODE_NOSCALE || _scaleMode == SCALEMODE_NOSCALE)) {
1316
1317 const movie_definition* md = _rootMovie->definition();
1318 log_debug("Going to or from scaleMode=noScale. Viewport:%dx%d Def:%dx%d",
1319 _stageWidth, _stageHeight,
1320 md->get_width_pixels(), md->get_height_pixels());
1321
1322 if ( _stageWidth != md->get_width_pixels()
1323 || _stageHeight != md->get_height_pixels() )
1324 {
1325 notifyResize = true;
1326 }
1327 }
1328
1329 _scaleMode = sm;
1330 callInterface(HostMessage(HostMessage::UPDATE_STAGE));
1331
1332 if (notifyResize) {
1333 as_object* stage = getBuiltinObject(*this, NSV::CLASS_STAGE);
1334 if (stage) {
1335 callMethod(stage, NSV::PROP_BROADCAST_MESSAGE, "onResize");
1336 }
1337 }
1338 }
1339
1340 void
setStageDisplayState(const DisplayState ds)1341 movie_root::setStageDisplayState(const DisplayState ds)
1342 {
1343 _displayState = ds;
1344
1345 as_object* stage = getBuiltinObject(*this, NSV::CLASS_STAGE);
1346 if (stage) {
1347 const bool fs = _displayState == DISPLAYSTATE_FULLSCREEN;
1348 callMethod(stage, NSV::PROP_BROADCAST_MESSAGE, "onFullScreen", fs);
1349 }
1350
1351 if (!_interfaceHandler) return; // No registered callback
1352
1353 HostMessage e(HostMessage::SET_DISPLAYSTATE, _displayState);
1354 callInterface(e);
1355 }
1356
1357 void
add_invalidated_bounds(InvalidatedRanges & ranges,bool force)1358 movie_root::add_invalidated_bounds(InvalidatedRanges& ranges, bool force)
1359 {
1360 if (isInvalidated()) {
1361 ranges.setWorld();
1362 return;
1363 }
1364
1365 for (Levels::reverse_iterator i=_movies.rbegin(), e=_movies.rend(); i!=e;
1366 ++i) {
1367 i->second->add_invalidated_bounds(ranges, force);
1368 }
1369 }
1370
1371 size_t
minPopulatedPriorityQueue() const1372 movie_root::minPopulatedPriorityQueue() const
1373 {
1374 for (size_t l = 0; l < PRIORITY_SIZE; ++l) {
1375 if (!_actionQueue[l].empty()) return l;
1376 }
1377 return PRIORITY_SIZE;
1378 }
1379
1380 size_t
processActionQueue(size_t lvl)1381 movie_root::processActionQueue(size_t lvl)
1382 {
1383 ActionQueue::value_type& q = _actionQueue[lvl];
1384
1385 assert(minPopulatedPriorityQueue() == lvl);
1386
1387 #ifdef GNASH_DEBUG
1388 static unsigned calls=0;
1389 ++calls;
1390 bool actionsToProcess = !q.empty();
1391 if (actionsToProcess) {
1392 log_debug("Processing %d actions in priority queue %d (call %u)",
1393 q.size(), lvl, calls);
1394 }
1395 #endif
1396
1397 // _actionQueue may be changed due to actions (appended-to)
1398 // this loop might be optimized by using an iterator
1399 // and a final call to .clear()
1400 while (!q.empty()) {
1401
1402 const std::unique_ptr<ExecutableCode> code(q.pop_front().release());
1403 code->execute();
1404
1405 size_t minLevel = minPopulatedPriorityQueue();
1406 if (minLevel < lvl) {
1407 #ifdef GNASH_DEBUG
1408 log_debug("Actions pushed in priority %d (< %d), restarting the scan (call %u)",
1409 minLevel, lvl, calls);
1410 #endif
1411 return minLevel;
1412 }
1413 }
1414
1415 assert(q.empty());
1416
1417 #ifdef GNASH_DEBUG
1418 if (actionsToProcess) {
1419 log_debug("Done processing actions in priority queue %d (call %u)",
1420 lvl, calls);
1421 }
1422 #endif
1423
1424 return minPopulatedPriorityQueue();
1425 }
1426
1427 void
flushHigherPriorityActionQueues()1428 movie_root::flushHigherPriorityActionQueues()
1429 {
1430 if (!processingActions()) {
1431 // only flush the actions queue when we are
1432 // processing the queue.
1433 // ie. we don't want to flush the queue
1434 // during executing user event handlers,
1435 // which are not pushed at the moment.
1436 return;
1437 }
1438
1439 if (_disableScripts) {
1440 /// cleanup anything pushed later..
1441 clear(_actionQueue);
1442 return;
1443 }
1444
1445 int lvl = minPopulatedPriorityQueue();
1446 while (lvl < _processingActionLevel) {
1447 lvl = processActionQueue(lvl);
1448 }
1449 }
1450
1451 void
addLoadableObject(as_object * obj,std::unique_ptr<IOChannel> str)1452 movie_root::addLoadableObject(as_object* obj, std::unique_ptr<IOChannel> str)
1453 {
1454 _loadCallbacks.emplace_back(std::move(str), obj);
1455 }
1456
1457 void
addAdvanceCallback(ActiveRelay * obj)1458 movie_root::addAdvanceCallback(ActiveRelay* obj)
1459 {
1460 _objectCallbacks.insert(obj);
1461 }
1462
1463 void
removeAdvanceCallback(ActiveRelay * obj)1464 movie_root::removeAdvanceCallback(ActiveRelay* obj)
1465 {
1466 _objectCallbacks.erase(obj);
1467 }
1468
1469 void
processActionQueue()1470 movie_root::processActionQueue()
1471 {
1472 if (_disableScripts) {
1473 /// cleanup anything pushed later..
1474 clear(_actionQueue);
1475 return;
1476 }
1477
1478 _processingActionLevel = minPopulatedPriorityQueue();
1479
1480 while (_processingActionLevel < PRIORITY_SIZE) {
1481 _processingActionLevel = processActionQueue(_processingActionLevel);
1482 }
1483
1484 // Cleanup the stack.
1485 _vm.getStack().clear();
1486 }
1487
1488 void
removeQueuedConstructor(MovieClip * target)1489 movie_root::removeQueuedConstructor(MovieClip* target)
1490 {
1491 ActionQueue::value_type& pr = _actionQueue[PRIORITY_CONSTRUCT];
1492 pr.erase_if(RemoveTargetCode(target));
1493 }
1494
1495 void
pushAction(std::unique_ptr<ExecutableCode> code,size_t lvl)1496 movie_root::pushAction(std::unique_ptr<ExecutableCode> code, size_t lvl)
1497 {
1498 assert(lvl < PRIORITY_SIZE);
1499 _actionQueue[lvl].push_back(code.release());
1500 }
1501
1502 void
pushAction(const action_buffer & buf,DisplayObject * target)1503 movie_root::pushAction(const action_buffer& buf, DisplayObject* target)
1504 {
1505 #ifdef GNASH_DEBUG
1506 log_debug("Pushed action buffer for target %s",
1507 target->getTargetPath());
1508 #endif
1509
1510 std::unique_ptr<ExecutableCode> code(new GlobalCode(buf, target));
1511
1512 _actionQueue[PRIORITY_DOACTION].push_back(code.release());
1513 }
1514
1515 void
executeAdvanceCallbacks()1516 movie_root::executeAdvanceCallbacks()
1517 {
1518 if (!_objectCallbacks.empty()) {
1519
1520 // We have two considerations:
1521 // 1. any update can change the active callbacks by removing or
1522 // adding to the original callbacks list.
1523 // 2. Additionally, an as_object may destroy its own Relay. This can
1524 // happen if the callback itself calls a native constructor on
1525 // an object that already has a Relay. If this is an ActiveRelay
1526 // registered with movie_root, a pointer to the destroyed object
1527 // will still be held, resulting in memory corruption. This is an
1528 // *extremely* unlikely case, but we are very careful!
1529 //
1530 // By copying to a new container we avoid errors caused by changes to
1531 // the original set (such as infinite recursions or invalidated
1532 // iterators). We also know that no as_object will be destroyed
1533 // during processing, even though its Relay may be.
1534 std::vector<as_object*> currentCallbacks;
1535
1536 std::transform(_objectCallbacks.begin(), _objectCallbacks.end(),
1537 std::back_inserter(currentCallbacks),
1538 std::bind(CreatePointer<as_object>(),
1539 std::bind(std::mem_fun(&ActiveRelay::owner),
1540 std::placeholders::_1)));
1541
1542 std::for_each(currentCallbacks.begin(), currentCallbacks.end(),
1543 ExecuteCallback());
1544 }
1545
1546 if (!_loadCallbacks.empty()) {
1547 _loadCallbacks.remove_if(
1548 std::mem_fun_ref(&movie_root::LoadCallback::processLoad));
1549 }
1550
1551 // _controlfd is set when running as a child process of a hosting
1552 // application. If it is set, we have to check the socket connection
1553 // for XML messages.
1554 if (_controlfd > 0) {
1555 std::unique_ptr<ExternalInterface::invoke_t> invoke =
1556 ExternalInterface::ExternalEventCheck(_controlfd);
1557 if (invoke) {
1558 if (processInvoke(invoke.get()) == false) {
1559 if (!invoke->name.empty()) {
1560 log_error(_("Couldn't process ExternalInterface Call %s"),
1561 invoke->name);
1562 }
1563 }
1564 }
1565 }
1566
1567 processActionQueue();
1568 }
1569
1570 bool
processInvoke(ExternalInterface::invoke_t * invoke)1571 movie_root::processInvoke(ExternalInterface::invoke_t *invoke)
1572 {
1573 GNASH_REPORT_FUNCTION;
1574
1575 if (!invoke || invoke->name.empty()) return false;
1576
1577 log_debug("Processing %s call from the Browser.", invoke->name);
1578
1579 std::stringstream ss; // ss is the response string
1580
1581 // These are the default methods used by ExternalInterface
1582 if (invoke->name == "Quit") {
1583 // Leave to the hosting application. If there isn't one or it
1584 // chooses not to exit, that's fine.
1585 if (_interfaceHandler) _interfaceHandler->exit();
1586
1587 } else if (invoke->name == "SetVariable") {
1588 MovieClip *mc = getLevel(0);
1589 as_object *obj = getObject(mc);
1590 VM &vm = getVM();
1591 std::string var = invoke->args[0].to_string();
1592 as_value &val = invoke->args[1] ;
1593 obj->set_member(getURI(vm, var), val);
1594 // SetVariable doesn't send a response
1595 } else if (invoke->name == "GetVariable") {
1596 MovieClip *mc = getLevel(0);
1597 as_object *obj = getObject(mc);
1598 VM &vm = getVM();
1599 as_environment timeline = mc->get_environment();
1600 as_environment::ScopeStack scope;
1601 as_object *container = NULL;
1602 std::string var = invoke->args[0].to_string();
1603 scope.push_back(obj);
1604 as_value val = getVariable(timeline, var, scope, &container);
1605 if (container != NULL) {
1606 // If the variable exists, GetVariable returns a string
1607 // representation of its value. Variable with undefined
1608 // or null value counts as exist too.
1609 ss << ExternalInterface::toXML(val.to_string(vm.getSWFVersion()));
1610 ss << std::endl;
1611 } else {
1612 // If the variable does not exist, GetVariable sends null value
1613 ss << ExternalInterface::toXML(as_value((as_object*)NULL));
1614 ss << std::endl;
1615 }
1616 } else if (invoke->name == "GotoFrame") {
1617 log_unimpl(_("ExternalInterface::GotoFrame()"));
1618 // GotoFrame doesn't send a response
1619 } else if (invoke->name == "IsPlaying") {
1620 const bool result =
1621 callInterface<bool>(HostMessage(HostMessage::EXTERNALINTERFACE_ISPLAYING));
1622 as_value val(result);
1623 ss << ExternalInterface::toXML(val);
1624 ss << std::endl;
1625 } else if (invoke->name == "LoadMovie") {
1626 log_unimpl(_("ExternalInterface::LoadMovie()"));
1627 // LoadMovie doesn't send a response
1628 } else if (invoke->name == "Pan") {
1629 std::string arg = invoke->args[0].to_string();
1630 arg += ":";
1631 arg += invoke->args[0].to_string();
1632 arg += ":";
1633 arg += invoke->args[1].to_string();
1634 arg += ":";
1635 arg += invoke->args[2].to_string();
1636 callInterface(HostMessage(HostMessage::EXTERNALINTERFACE_PAN, arg));
1637 // Pan doesn't send a response
1638 } else if (invoke->name == "PercentLoaded") {
1639 MovieClip *mc = getLevel(0);
1640 int loaded = mc->get_bytes_loaded();
1641 int total = mc->get_bytes_total();
1642 int percent = 0;
1643 if (total > 0) { /* avoid division by zero */
1644 percent = 100 * loaded / total;
1645 }
1646 as_value val(percent);
1647 // PercentLoaded sends the percentage
1648 ss << ExternalInterface::toXML(val);
1649 ss << std::endl;
1650 } else if (invoke->name == "Play") {
1651 callInterface(HostMessage(HostMessage::EXTERNALINTERFACE_PLAY));
1652 // Play doesn't send a response
1653 } else if (invoke->name == "Rewind") {
1654 callInterface(HostMessage(HostMessage::EXTERNALINTERFACE_REWIND));
1655 // Rewind doesn't send a response
1656 } else if (invoke->name == "SetZoomRect") {
1657 std::string arg = invoke->args[0].to_string();
1658 arg += ":";
1659 arg += invoke->args[0].to_string();
1660 arg += ":";
1661 arg += invoke->args[1].to_string();
1662 arg += ":";
1663 arg += invoke->args[2].to_string();
1664 arg += ":";
1665 arg += invoke->args[3].to_string();
1666 callInterface(HostMessage(HostMessage::EXTERNALINTERFACE_SETZOOMRECT, arg));
1667 // SetZoomRect doesn't send a response
1668 } else if (invoke->name == "StopPlay") {
1669 callInterface(HostMessage(HostMessage::EXTERNALINTERFACE_STOPPLAY));
1670 // StopPlay doesn't send a response
1671 } else if (invoke->name == "Zoom") {
1672 std::string var = invoke->args[0].to_string();
1673 callInterface(HostMessage(HostMessage::EXTERNALINTERFACE_ZOOM, var));
1674 // Zoom doesn't send a response
1675 } else if (invoke->name == "TotalFrames") {
1676 MovieClip *mc = getLevel(0);
1677 as_value val(mc->get_loaded_frames());
1678 // TotalFrames sends the number of frames in the movie
1679 ss << ExternalInterface::toXML(val);
1680 ss << std::endl;
1681 } else {
1682 callExternalCallback(invoke->name, invoke->args);
1683 return true;
1684 }
1685
1686 if (!ss.str().empty()) {
1687 if (_hostfd >= 0) {
1688 log_debug("Attempt to write response to ExternalInterface "
1689 "requests fd %d", _hostfd);
1690 int ret = write(_hostfd, ss.str().c_str(), ss.str().size());
1691 if (ret == -1) {
1692 log_error(_("Could not write to user-provided host requests "
1693 "fd %d: %s"), _hostfd, std::strerror(errno));
1694 }
1695 }
1696 } else {
1697 log_debug("No response needed for %s request", invoke->name);
1698 }
1699
1700 return true;
1701 }
1702
1703 void
executeTimers()1704 movie_root::executeTimers()
1705 {
1706 #ifdef GNASH_DEBUG_TIMERS_EXPIRATION
1707 log_debug("Checking %d timers for expiry", _intervalTimers.size());
1708 #endif
1709
1710 // Don't do anything if we have no timers, just return so we don't
1711 // waste cpu cycles.
1712 if (_intervalTimers.empty()) {
1713 return;
1714 }
1715
1716 unsigned long now = _vm.getTime();
1717
1718 typedef std::multimap<unsigned long, Timer*>
1719 ExpiredTimers;
1720
1721 ExpiredTimers expiredTimers;
1722
1723 for (TimerMap::iterator it = _intervalTimers.begin(),
1724 itEnd = _intervalTimers.end(); it != itEnd; ) {
1725
1726 TimerMap::iterator nextIterator = it;
1727 ++nextIterator;
1728
1729 Timer* timer = it->second.get();
1730
1731 if (timer->cleared()) {
1732 // this timer was cleared, erase it
1733 _intervalTimers.erase(it);
1734 }
1735 else {
1736 unsigned long elapsed;
1737 if (timer->expired(now, elapsed)) {
1738 expiredTimers.insert(std::make_pair(elapsed, timer));
1739 }
1740 }
1741
1742 it = nextIterator;
1743 }
1744
1745 foreachSecond(expiredTimers.begin(), expiredTimers.end(),
1746 &Timer::executeAndReset);
1747
1748 if (!expiredTimers.empty())
1749 processActionQueue();
1750 }
1751
1752 void
markReachableResources() const1753 movie_root::markReachableResources() const
1754 {
1755 _vm.markReachableResources();
1756
1757 foreachSecond(_movies.rbegin(), _movies.rend(), &MovieClip::setReachable);
1758
1759 // Mark original top-level movie
1760 // This should always be in _movies, but better make sure
1761 assert(_rootMovie);
1762 _rootMovie->setReachable();
1763
1764 // Mark mouse entities
1765 _mouseButtonState.markReachableResources();
1766
1767 // Mark timer targets
1768 foreachSecond(_intervalTimers.begin(), _intervalTimers.end(),
1769 &Timer::markReachableResources);
1770
1771 std::for_each(_objectCallbacks.begin(), _objectCallbacks.end(),
1772 std::mem_fun(&ActiveRelay::setReachable));
1773 std::for_each(_loadCallbacks.begin(), _loadCallbacks.end(),
1774 std::mem_fun_ref(&movie_root::LoadCallback::setReachable));
1775
1776 // Mark LoadMovieRequest handlers as reachable
1777 _movieLoader.setReachable();
1778
1779 // Mark ExternalInterface callbacks and instances as reachable
1780 for (const auto& method : _externalCallbackMethods) {
1781 if (method.second) {
1782 method.second->setReachable();
1783 }
1784 }
1785 for (const auto& instance : _externalCallbackInstances) {
1786 if (instance.second) {
1787 instance.second->setReachable();
1788 }
1789 }
1790
1791 // Mark resources reachable by queued action code
1792 for (size_t lvl = 0; lvl < PRIORITY_SIZE; ++lvl)
1793 {
1794 const ActionQueue::value_type& q = _actionQueue[lvl];
1795 std::for_each(q.begin(), q.end(),
1796 std::mem_fun_ref(&ExecutableCode::markReachableResources));
1797 }
1798
1799 if (_currentFocus) _currentFocus->setReachable();
1800
1801 // Mark DisplayObject being dragged, if any
1802 if (_dragState) _dragState->markReachableResources();
1803
1804 // NOTE: cleanupDisplayList() should have cleaned up all
1805 // unloaded live characters. The remaining ones should be marked
1806 // by their parents.
1807 #if ( GNASH_PARANOIA_LEVEL > 1 ) || defined(ALLOW_GC_RUN_DURING_ACTIONS_EXECUTION)
1808 for (LiveChars::const_iterator i=_liveChars.begin(), e=_liveChars.end();
1809 i!=e; ++i) {
1810 #ifdef ALLOW_GC_RUN_DURING_ACTIONS_EXECUTION
1811 (*i)->setReachable();
1812 #else
1813 assert((*i)->isReachable());
1814 #endif
1815 }
1816 #endif
1817
1818 foreachSecond(_registeredClasses.begin(), _registeredClasses.end(), &as_function::setReachable);
1819 }
1820
1821 InteractiveObject*
getTopmostMouseEntity(std::int32_t x,std::int32_t y) const1822 movie_root::getTopmostMouseEntity(std::int32_t x, std::int32_t y) const
1823 {
1824 for (Levels::const_reverse_iterator i=_movies.rbegin(), e=_movies.rend();
1825 i != e; ++i)
1826 {
1827 InteractiveObject* ret = i->second->topmostMouseEntity(x, y);
1828 if (ret) return ret;
1829 }
1830
1831 return nullptr;
1832 }
1833
1834 const DisplayObject *
findDropTarget(std::int32_t x,std::int32_t y,DisplayObject * dragging) const1835 movie_root::findDropTarget(std::int32_t x, std::int32_t y,
1836 DisplayObject* dragging) const
1837 {
1838 for (Levels::const_reverse_iterator i=_movies.rbegin(), e=_movies.rend();
1839 i!=e; ++i) {
1840
1841 const DisplayObject* ret = i->second->findDropTarget(x, y, dragging);
1842 if (ret) return ret;
1843 }
1844 return nullptr;
1845 }
1846
1847 /// This should store a callback object in movie_root.
1848 void
addExternalCallback(const std::string & name,as_object * callback,as_object * instance)1849 movie_root::addExternalCallback(const std::string& name, as_object* callback,
1850 as_object* instance)
1851 {
1852 // Store registered callback and instance reference for later use
1853 // by callExternalCallback()
1854 if(_externalCallbackMethods.count(name)>0) {
1855 _externalCallbackMethods.erase(name);
1856 _externalCallbackInstances.erase(name);
1857 }
1858 _externalCallbackMethods.insert(
1859 std::pair<std::string, as_object*>(name,callback)
1860 );
1861 _externalCallbackInstances.insert(
1862 std::pair<std::string, as_object*>(name,instance)
1863 );
1864
1865 // When an external callback is added, we have to notify the plugin
1866 // that this method is available.
1867 if (_hostfd >= 0) {
1868 std::vector<as_value> fnargs;
1869 fnargs.push_back(name);
1870 std::string msg = ExternalInterface::makeInvoke("addMethod", fnargs);
1871
1872 const size_t ret = ExternalInterface::writeBrowser(_hostfd, msg);
1873 if (ret != msg.size()) {
1874 log_error(_("Could not write to browser fd #%d: %s"),
1875 _hostfd, std::strerror(errno));
1876 }
1877 }
1878 }
1879
1880 /// This calls a JavaScript method in the web page
1881 ///
1882 /// @example "ExternalInterace::call message"
1883 ///
1884 /// <pre>
1885 /// <invoke name="methodname" returntype="xml">
1886 /// <arguments></arguments>
1887 /// ...
1888 /// <arguments></arguments>
1889 /// </invoke>
1890 ///
1891 /// May return any supported type like Number or String in XML format.
1892 ///
1893 /// </pre>
1894 std::string
callExternalJavascript(const std::string & name,const std::vector<as_value> & fnargs)1895 movie_root::callExternalJavascript(const std::string &name,
1896 const std::vector<as_value> &fnargs)
1897 {
1898 std::string result;
1899 // If the browser is connected, we send an Invoke message to the
1900 // browser.
1901 if (_controlfd >= 0 && _hostfd >= 0) {
1902 std::string msg = ExternalInterface::makeInvoke(name, fnargs);
1903
1904 const size_t ret = ExternalInterface::writeBrowser(_hostfd, msg);
1905 if (ret != msg.size()) {
1906 log_error(_("Could not write to browser fd #%d: %s"),
1907 _hostfd, std::strerror(errno));
1908 } else {
1909 // Now read the response from the browser after it's exectuted
1910 // the JavaScript function.
1911 result = ExternalInterface::readBrowser(_controlfd);
1912 }
1913 }
1914
1915 return result;
1916 }
1917
1918 // Call one of the registered callbacks, and return the result to
1919 // Javascript in the browser.
1920 std::string
callExternalCallback(const std::string & name,const std::vector<as_value> & fnargs)1921 movie_root::callExternalCallback(const std::string &name,
1922 const std::vector<as_value> &fnargs)
1923 {
1924 ExternalCallbackMethods::iterator method_iterator;
1925 ExternalCallbackInstances::iterator instance_iterator;
1926 as_object *method;
1927 as_object *instance;
1928 fn_call::Args args;
1929 as_value val;
1930
1931 // Look up for ActionScript function registered as callback
1932 method_iterator = _externalCallbackMethods.find(name);
1933 if (method_iterator == _externalCallbackMethods.end()) {
1934 val.set_undefined();
1935 } else {
1936 method = method_iterator->second;
1937
1938 // Look up for Object instance to use as "this" in the callback
1939 instance_iterator = _externalCallbackInstances.find(name);
1940 if (instance_iterator == _externalCallbackInstances.end()) {
1941 instance = as_value((as_object*)NULL).to_object(getVM());
1942 }
1943 else instance = instance_iterator->second;
1944
1945 // Use _global object as "this" instance if the callback is originally
1946 // registered with null or undefined one.
1947 if (instance == NULL) {
1948 instance = &getGlobal(*method);
1949 }
1950
1951 // Populate function call arguments
1952 for (std::vector<as_value>::const_iterator args_iterator
1953 = fnargs.begin();
1954 args_iterator != fnargs.end();
1955 args_iterator ++)
1956 {
1957 args += *args_iterator;
1958 }
1959
1960 // Call the registered callback
1961 val=invoke(as_value(method), as_environment(getVM()), instance, args);
1962 }
1963
1964 std::string result;
1965 result = ExternalInterface::toXML(val);
1966
1967 // If the browser is connected, we send an Invoke message to the
1968 // browser.
1969 if (_hostfd >= 0) {
1970 std::stringstream ss;
1971 size_t ret;
1972
1973 ss << result;
1974 ss << std::endl;
1975 ret = ExternalInterface::writeBrowser(_hostfd, ss.str());
1976 if (ret != ss.str().size()) {
1977 log_error(_("Could not write to browser fd #%d: %s"),
1978 _hostfd, std::strerror(errno));
1979 }
1980 }
1981
1982 return result;
1983 }
1984
1985 void
removeButton(Button * listener)1986 movie_root::removeButton(Button* listener)
1987 {
1988 _buttonListeners.remove(listener);
1989 }
1990
1991 void
registerButton(Button * listener)1992 movie_root::registerButton(Button* listener)
1993 {
1994 if (std::find(_buttonListeners.begin(), _buttonListeners.end(), listener)
1995 != _buttonListeners.end()) {
1996 return;
1997 }
1998 _buttonListeners.push_front(listener);
1999 }
2000
2001 void
cleanupDisplayList()2002 movie_root::cleanupDisplayList()
2003 {
2004 //#define GNASH_DEBUG_INSTANCE_LIST 1
2005
2006 #ifdef GNASH_DEBUG_INSTANCE_LIST
2007 static size_t maxLiveChars = 0;
2008 #endif
2009
2010 // Let every sprite cleanup the local DisplayList
2011 //
2012 // TODO: we might skip this additional scan by delegating
2013 // cleanup of the local DisplayLists in the ::display
2014 // method of each sprite, but that will introduce
2015 // problems when we implement skipping ::display()
2016 // when late on FPS. Alternatively we may have the
2017 // MovieClip::markReachableResources take care
2018 // of cleaning up unloaded... but that will likely
2019 // introduce problems when allowing the GC to run
2020 // at arbitrary times.
2021 // The invariant to keep is that cleanup of unloaded DisplayObjects
2022 // in local display lists must happen at the *end* of global action
2023 // queue processing.
2024 foreachSecond(_movies.rbegin(), _movies.rend(),
2025 &MovieClip::cleanupDisplayList);
2026
2027 // Now remove from the instance list any unloaded DisplayObject
2028 // Note that some DisplayObjects may be unloaded but not yet destroyed,
2029 // in this case we'll also destroy them, which in turn might unload
2030 // further DisplayObjects, maybe already scanned, so we keep scanning
2031 // the list until no more unloaded-but-non-destroyed DisplayObjects
2032 // are found.
2033 // Keeping unloaded-but-non-destroyed DisplayObjects wouldn't really hurt
2034 // in that ::advanceLiveChars would skip any unloaded DisplayObjects.
2035 // Still, the more we remove the less work GC has to do...
2036 //
2037
2038 bool needScan;
2039 #ifdef GNASH_DEBUG_DLIST_CLEANUP
2040 int scansCount = 0;
2041 #endif
2042 do {
2043 #ifdef GNASH_DEBUG_DLIST_CLEANUP
2044 scansCount++;
2045 int cleaned =0;
2046 #endif
2047 needScan=false;
2048
2049 // Remove unloaded MovieClips from the _liveChars list
2050 _liveChars.remove_if([&](MovieClip* ch) {
2051 if (ch->unloaded()) {
2052 // the sprite might have been destroyed already
2053 // by effect of an unload() call with no onUnload
2054 // handlers available either in self or child
2055 // DisplayObjects
2056 if (!ch->isDestroyed()) {
2057
2058 #ifdef GNASH_DEBUG_DLIST_CLEANUP
2059 cout << ch->getTarget() << "(" << typeName(*ch) <<
2060 ") was unloaded but not destroyed, destroying now" <<
2061 endl;
2062 #endif
2063 ch->destroy();
2064 // destroy() might mark already-scanned chars as unloaded
2065 needScan = true;
2066 }
2067 #ifdef GNASH_DEBUG_DLIST_CLEANUP
2068 else {
2069 cout << ch->getTarget() << "(" << typeName(*ch) <<
2070 ") was unloaded and destroyed" << endl;
2071 }
2072 #endif
2073
2074 #ifdef GNASH_DEBUG_DLIST_CLEANUP
2075 cleaned++;
2076 #endif
2077 return true;
2078 }
2079 return false;
2080 });
2081
2082 #ifdef GNASH_DEBUG_DLIST_CLEANUP
2083 cout << " Scan " << scansCount << " cleaned " << cleaned <<
2084 " instances" << endl;
2085 #endif
2086 } while (needScan);
2087
2088 #ifdef GNASH_DEBUG_INSTANCE_LIST
2089 size_t count = std::distance(begin(_liveChars), end(_liveChars));
2090 if (count > maxLiveChars) {
2091 maxLiveChars = count;
2092 log_debug("Global instance list grew to %d entries", maxLiveChars);
2093 }
2094 #endif
2095 }
2096
2097 void
advanceLiveChars()2098 movie_root::advanceLiveChars()
2099 {
2100 #ifdef GNASH_DEBUG
2101 log_debug("---- movie_root::advance: %d live DisplayObjects in the global list",
2102 _liveChars.size());
2103 #endif
2104
2105 // Advance all characters, then notify them.
2106 LiveChars::iterator it;
2107 for (MovieClip* liveChar : _liveChars) {
2108 advanceLiveChar(liveChar);
2109 }
2110 for (MovieClip* liveChar : _liveChars) {
2111 notifyLoad(liveChar);
2112 }
2113 }
2114
2115 void
set_background_color(const rgba & color)2116 movie_root::set_background_color(const rgba& color)
2117 {
2118 if (m_background_color_set) return;
2119 m_background_color_set = true;
2120
2121 rgba newcolor = color;
2122 newcolor.m_a = m_background_color.m_a;
2123
2124 if (m_background_color != newcolor) {
2125 setInvalidated();
2126 m_background_color = newcolor;
2127 }
2128 }
2129
2130 void
set_background_alpha(float alpha)2131 movie_root::set_background_alpha(float alpha)
2132 {
2133 const std::uint8_t newAlpha = clamp<int>(frnd(alpha * 255.0f), 0, 255);
2134
2135 if (m_background_color.m_a != newAlpha) {
2136 setInvalidated();
2137 m_background_color.m_a = newAlpha;
2138 }
2139 }
2140
2141 DisplayObject*
findCharacterByTarget(const std::string & tgtstr) const2142 movie_root::findCharacterByTarget(const std::string& tgtstr) const
2143 {
2144 if (tgtstr.empty()) return nullptr;
2145
2146 // NOTE: getRootMovie() would be problematic in case the original
2147 // root movie is replaced by a load to _level0...
2148 // (but I guess we'd also drop loadMovie requests in that
2149 // case... just not tested)
2150 as_object* o = getObject(_movies.begin()->second);
2151 assert(o);
2152
2153 std::string::size_type from = 0;
2154 while (std::string::size_type to = tgtstr.find('.', from)) {
2155 std::string part(tgtstr, from, to - from);
2156
2157 // TODO: there is surely a cleaner way to implement path finding.
2158 const ObjectURI& uri = getURI(_vm, part);
2159 o = o->displayObject() ?
2160 o->displayObject()->pathElement(uri) :
2161 getPathElement(*o, uri);
2162
2163 if (!o) {
2164 #ifdef GNASH_DEBUG_TARGET_RESOLUTION
2165 log_debug("Evaluating DisplayObject target path: element"
2166 "'%s' of path '%s' not found", part, tgtstr);
2167 #endif
2168 return nullptr;
2169 }
2170 if (to == std::string::npos) break;
2171 from = to + 1;
2172 }
2173 return get<DisplayObject>(o);
2174 }
2175
2176 void
getURL(const std::string & urlstr,const std::string & target,const std::string & data,MovieClip::VariablesMethod method)2177 movie_root::getURL(const std::string& urlstr, const std::string& target,
2178 const std::string& data, MovieClip::VariablesMethod method)
2179 {
2180 log_network("%s: HOSTFD is %d", __FUNCTION__, _hostfd);
2181
2182 if (_hostfd < 0) {
2183 /// If there is no hosting application, call the URL launcher. For
2184 /// safety, we resolve the URL against the base URL for this run.
2185 /// The data is not sent at all.
2186 URL url(urlstr, _runResources.streamProvider().baseURL());
2187
2188 gnash::RcInitFile& rcfile = gnash::RcInitFile::getDefaultInstance();
2189 std::string command = rcfile.getURLOpenerFormat();
2190
2191 /// Try to avoid letting flash movies execute
2192 /// arbitrary commands (sic).
2193 ///
2194 /// NOTE: it is assumed that the user-provided command
2195 /// puts the url place-holder within single quotes.
2196 /// Failing that, there will be the possibility
2197 /// for malicious SWF files to run arbitrary commands.
2198 ///
2199 ///
2200 /// Check safety of user provided command
2201 ///
2202 /// TODO: improve this check
2203 /// - quote nested in double quote
2204 /// - %u after second quote
2205 /// - use regexp ?
2206 /// TODO: check only once
2207 ///
2208 bool command_is_safe = false;
2209 do {
2210 std::string::size_type loc = command.find('\'');
2211 if ( loc == std::string::npos ) break;
2212 loc = command.find("%u", loc);
2213 if ( loc == std::string::npos ) break;
2214 loc = command.find('\'', loc);
2215 if ( loc == std::string::npos ) break;
2216 command_is_safe = true;
2217 } while (0);
2218
2219 if ( ! command_is_safe ) {
2220 log_error("The '%%u' token in urlOpenerFormat rc directive should be within single quotes");
2221 return;
2222 }
2223
2224 std::string safeurl = urlstr;
2225 boost::replace_all(safeurl, "'", "'\\''");
2226
2227 boost::replace_all(command, "%u", safeurl);
2228
2229 log_debug("Launching URL: %s", command);
2230 const int ret = std::system(command.c_str());
2231 if (ret == -1) {
2232 log_error(_("Fork failed launching URL opener '%s'"), command);
2233 }
2234 return;
2235 }
2236
2237 /// This is when there is a hosting application.
2238 std::vector<as_value> fnargs;
2239 // The first argument we push on the stack is the URL
2240 fnargs.emplace_back(urlstr);
2241
2242 // The second argument we push is the method
2243 switch (method) {
2244 case MovieClip::METHOD_POST:
2245 fnargs.emplace_back("POST");
2246 break;
2247 case MovieClip::METHOD_GET:
2248 fnargs.emplace_back("GET");
2249 break;
2250 case MovieClip::METHOD_NONE:
2251 default:
2252 fnargs.emplace_back("GET");
2253 break;
2254 }
2255
2256 // The third argument is the target, which is something like _blank
2257 // or _self.
2258 if (!target.empty()) {
2259 fnargs.emplace_back(target);
2260 }
2261 // Add any data as the optional 4th argument
2262 if (!data.empty()) {
2263 // We have to write a value here so the data field is the fourth
2264 if (target.empty()) {
2265 fnargs.emplace_back("none");
2266 }
2267 fnargs.emplace_back(data);
2268 }
2269
2270 // TODO: should mutex-protect this ?
2271 // NOTE: we are assuming the hostfd is set in blocking mode here..
2272
2273 log_debug("Attempt to write geturl requests fd #%d", _hostfd);
2274
2275 std::string msg = ExternalInterface::makeInvoke("getURL", fnargs);
2276
2277 const size_t ret = ExternalInterface::writeBrowser(_hostfd, msg);
2278 if (ret < msg.size()) {
2279 log_error(_("Could only write %d bytes to fd #%d"),
2280 ret, _hostfd);
2281 }
2282 }
2283
2284 void
setScriptLimits(std::uint16_t recursion,std::uint16_t timeout)2285 movie_root::setScriptLimits(std::uint16_t recursion, std::uint16_t timeout)
2286 {
2287 if ( recursion == _recursionLimit && _timeoutLimit == timeout ) {
2288 // avoid the debug log...
2289 return;
2290 }
2291
2292 if (RcInitFile::getDefaultInstance().lockScriptLimits()) {
2293 LOG_ONCE(log_debug("SWF ScriptLimits tag attempting to set "
2294 "recursionLimit=%1% and scriptsTimeout=%2% ignored "
2295 "as per rcfile directive", recursion, timeout) );
2296 return;
2297 }
2298
2299 // This tag reported in some sources to be ignored for movies
2300 // below SWF7. However, on Linux with PP version 9, the tag
2301 // takes effect on SWFs of any version.
2302 log_debug("Setting script limits: max recursion %d, "
2303 "timeout %d seconds", recursion, timeout);
2304
2305 _recursionLimit = recursion;
2306 _timeoutLimit = timeout;
2307
2308 }
2309
2310
2311 #ifdef USE_SWFTREE
2312 void
getMovieInfo(InfoTree & tr,InfoTree::iterator it)2313 movie_root::getMovieInfo(InfoTree& tr, InfoTree::iterator it)
2314 {
2315 // Stage
2316 const movie_definition* def = _rootMovie->definition();
2317 assert(def);
2318
2319 it = tr.insert(it, std::make_pair("Stage Properties", ""));
2320
2321 InfoTree::iterator localIter = tr.append_child(it,
2322 std::make_pair("Root VM version",
2323 def->isAS3() ? "AVM2 (unsupported)" : "AVM1"));
2324
2325 std::ostringstream os;
2326 os << "SWF " << def->get_version();
2327 localIter = tr.append_child(it, std::make_pair("Root SWF version",
2328 os.str()));
2329 localIter = tr.append_child(it, std::make_pair("URL", def->get_url()));
2330
2331 // Is there a sychronizating sound or not?
2332 localIter = tr.append_child(it, std::make_pair("Streaming sound",
2333 _timelineSound ? "yes" : "no"));
2334
2335 // TODO: format this better?
2336 localIter = tr.append_child(it, std::make_pair("Descriptive metadata",
2337 def->getDescriptiveMetadata()));
2338
2339 /// Stage: real dimensions.
2340 os.str("");
2341 os << def->get_width_pixels() <<
2342 "x" << def->get_height_pixels();
2343 localIter = tr.append_child(it, std::make_pair("Real dimensions",
2344 os.str()));
2345
2346 /// Stage: rendered dimensions.
2347 os.str("");
2348 os << _stageWidth << "x" << _stageHeight;
2349 localIter = tr.append_child(it, std::make_pair("Rendered dimensions",
2350 os.str()));
2351
2352 // Stage: scripts state (enabled/disabled)
2353 localIter = tr.append_child(it, std::make_pair("Scripts",
2354 _disableScripts ? " disabled" : "enabled"));
2355
2356 getCharacterTree(tr, it);
2357 }
2358
2359 void
getCharacterTree(InfoTree & tr,InfoTree::iterator it)2360 movie_root::getCharacterTree(InfoTree& tr, InfoTree::iterator it)
2361 {
2362 InfoTree::iterator localIter;
2363
2364 /// Stage: number of live MovieClips.
2365 std::ostringstream os;
2366 os << std::distance(begin(_liveChars), end(_liveChars));
2367 localIter = tr.append_child(it, std::make_pair(_("Live MovieClips"),
2368 os.str()));
2369
2370 /// DisplayObject tree
2371 for (Levels::const_iterator i = _movies.begin(), e = _movies.end();
2372 i != e; ++i) {
2373 i->second->getMovieInfo(tr, localIter);
2374 }
2375 }
2376
2377 #endif
2378
2379 void
handleFsCommand(const std::string & cmd,const std::string & arg) const2380 movie_root::handleFsCommand(const std::string& cmd, const std::string& arg)
2381 const
2382 {
2383 if (_fsCommandHandler) _fsCommandHandler->notify(cmd, arg);
2384 }
2385
2386 bool
isLevelTarget(int version,const std::string & name,unsigned int & levelno)2387 isLevelTarget(int version, const std::string& name, unsigned int& levelno)
2388 {
2389 if (version > 6) {
2390 if (name.compare(0, 6, "_level")) return false;
2391 }
2392 else {
2393 StringNoCaseEqual noCaseCmp;
2394 if (!noCaseCmp(name.substr(0, 6), "_level")) return false;
2395 }
2396
2397 if (name.find_first_not_of("0123456789", 7) != std::string::npos) {
2398 return false;
2399 }
2400 // getting 0 here for "_level" is intentional
2401 levelno = std::strtoul(name.c_str() + 6, nullptr, 0);
2402 return true;
2403 }
2404
2405 short
stringToStageAlign(const std::string & str)2406 stringToStageAlign(const std::string& str)
2407 {
2408 short am = 0;
2409
2410 // Easy enough to do bitwise - std::bitset is not
2411 // really necessary!
2412 if (str.find_first_of("lL") != std::string::npos) {
2413 am |= 1 << movie_root::STAGE_ALIGN_L;
2414 }
2415
2416 if (str.find_first_of("tT") != std::string::npos) {
2417 am |= 1 << movie_root::STAGE_ALIGN_T;
2418 }
2419
2420 if (str.find_first_of("rR") != std::string::npos) {
2421 am |= 1 << movie_root::STAGE_ALIGN_R;
2422 }
2423
2424 if (str.find_first_of("bB") != std::string::npos) {
2425 am |= 1 << movie_root::STAGE_ALIGN_B;
2426 }
2427
2428 return am;
2429
2430 }
2431
2432 void
setReachable() const2433 movie_root::LoadCallback::setReachable() const
2434 {
2435 _obj->setReachable();
2436 }
2437
2438 bool
processLoad()2439 movie_root::LoadCallback::processLoad()
2440 {
2441 if (!_stream) {
2442 callMethod(_obj, NSV::PROP_ON_DATA, as_value());
2443 return true;
2444 }
2445
2446 const size_t chunksize = 65535;
2447 std::uint8_t chunk[chunksize];
2448
2449 size_t actuallyRead = _stream->readNonBlocking(chunk, chunksize);
2450
2451 // We must still call onData if the stream is in error condition, e.g.
2452 // when an HTTP 404 error is returned.
2453 if (_stream->bad()) {
2454 callMethod(_obj, NSV::PROP_ON_DATA, as_value());
2455 return true;
2456 }
2457
2458 if (actuallyRead) {
2459
2460 // set total size only on first read
2461 if (_buf.empty()) {
2462 _obj->set_member(NSV::PROP_uBYTES_TOTAL, _stream->size());
2463 }
2464
2465 _buf.append(chunk, actuallyRead);
2466
2467 _obj->set_member(NSV::PROP_uBYTES_LOADED, _buf.size());
2468
2469 log_debug("LoadableObject Loaded %d bytes, reaching %d/%d",
2470 actuallyRead, _buf.size(), _stream->size());
2471 }
2472
2473 // We haven't finished till EOF
2474 if (!_stream->eof()) return false;
2475
2476 log_debug("LoadableObject reached EOF (%d/%d loaded)",
2477 _buf.size(), _stream->size());
2478
2479 // got nothing, won't bother BOFs of nulls
2480 if (_buf.empty()) {
2481 callMethod(_obj, NSV::PROP_ON_DATA, as_value());
2482 return true;
2483 }
2484
2485 // Terminate the string
2486 _buf.appendByte('\0');
2487
2488 // Strip BOM, if any.
2489 // See http://savannah.gnu.org/bugs/?19915
2490 utf8::TextEncoding encoding;
2491 size_t size = _buf.size();
2492
2493 // NOTE: the call below will possibly change 'size' parameter
2494 const char* bufptr = utf8::stripBOM(
2495 reinterpret_cast<const char*>(_buf.data()), size, encoding);
2496 if (encoding != utf8::encUTF8 && encoding != utf8::encUNSPECIFIED) {
2497 log_unimpl(_("%s to UTF8 conversion in LoadableObject input parsing"),
2498 utf8::textEncodingName(encoding));
2499 }
2500
2501 // NOTE: Data copy here !!
2502 as_value dataVal(bufptr);
2503
2504 // NOTE: we could release memory associated
2505 // with the buffer here, before invoking a new method,
2506 // but at the time of writing there's no method of SimpleBuffer
2507 // providing memory release except destruction. Will be
2508 // destroyed as soon as we return though...
2509
2510 callMethod(_obj, NSV::PROP_ON_DATA, std::move(dataVal));
2511
2512 return true;
2513 }
2514
2515 void
callInterface(const HostInterface::Message & e) const2516 movie_root::callInterface(const HostInterface::Message& e) const
2517 {
2518 if (!_interfaceHandler) {
2519 log_error(_("Hosting application registered no callback for events/queries, can't call %s(%s)"));
2520 return;
2521 }
2522 _interfaceHandler->call(e);
2523 }
2524
2525
2526 inline bool
testInvariant() const2527 movie_root::testInvariant() const
2528 {
2529 // TODO: fill this function !
2530 // The _movies map can not invariantably
2531 // be non-empty as the stage is autonomous
2532 // itself
2533 //assert( ! _movies.empty() );
2534
2535 return true;
2536 }
2537
2538 namespace {
2539
2540 // Return whether any action triggered by this event requires display redraw.
2541 //
2542 /// TODO: make this code more readable !
2543 bool
generate_mouse_button_events(movie_root & mr,MouseButtonState & ms)2544 generate_mouse_button_events(movie_root& mr, MouseButtonState& ms)
2545 {
2546 // Did this event trigger any action that needs redisplay ?
2547 bool need_redisplay = false;
2548
2549 // TODO: have mouseEvent return
2550 // whether the action must trigger
2551 // a redraw.
2552
2553 if (ms.wasDown) {
2554 // TODO: Handle trackAsMenu dragOver
2555 // Handle onDragOut, onDragOver
2556 if (!ms.wasInsideActiveEntity) {
2557
2558 if (ms.topmostEntity == ms.activeEntity) {
2559
2560 // onDragOver
2561 if (ms.activeEntity) {
2562 ms.activeEntity->mouseEvent(event_id(event_id::DRAG_OVER));
2563 need_redisplay=true;
2564 }
2565 ms.wasInsideActiveEntity = true;
2566 }
2567 }
2568 else if (ms.topmostEntity != ms.activeEntity) {
2569 // onDragOut
2570 if (ms.activeEntity) {
2571 ms.activeEntity->mouseEvent(event_id(event_id::DRAG_OUT));
2572 need_redisplay=true;
2573 }
2574 ms.wasInsideActiveEntity = false;
2575 }
2576
2577 // Handle onRelease, onReleaseOutside
2578 if (!ms.isDown) {
2579 // Mouse button just went up.
2580 ms.wasDown = false;
2581
2582 if (ms.activeEntity) {
2583 if (ms.wasInsideActiveEntity) {
2584 // onRelease
2585 ms.activeEntity->mouseEvent(event_id(event_id::RELEASE));
2586 need_redisplay = true;
2587 }
2588 else {
2589 // TODO: Handle trackAsMenu
2590 // onReleaseOutside
2591 ms.activeEntity->mouseEvent(
2592 event_id(event_id::RELEASE_OUTSIDE));
2593 // We got out of active entity
2594 ms.activeEntity = nullptr; // so we don't get RollOut next...
2595 need_redisplay = true;
2596 }
2597 }
2598 }
2599 return need_redisplay;
2600 }
2601
2602 else {
2603 // New active entity is whatever is below the mouse right now.
2604 if (ms.topmostEntity != ms.activeEntity) {
2605 // onRollOut
2606 if (ms.activeEntity) {
2607 ms.activeEntity->mouseEvent(event_id(event_id::ROLL_OUT));
2608 need_redisplay=true;
2609 }
2610
2611 ms.activeEntity = ms.topmostEntity;
2612
2613 // onRollOver
2614 if (ms.activeEntity) {
2615 ms.activeEntity->mouseEvent(event_id(event_id::ROLL_OVER));
2616 need_redisplay=true;
2617 }
2618
2619 ms.wasInsideActiveEntity = true;
2620 }
2621
2622 // mouse button press
2623 if (ms.isDown) {
2624 // onPress
2625
2626 // Try setting focus on the new DisplayObject. This will handle
2627 // all necessary events and removal of current focus.
2628 // Do not set focus to NULL.
2629 if (ms.activeEntity) {
2630 mr.setFocus(ms.activeEntity);
2631
2632 ms.activeEntity->mouseEvent(event_id(event_id::PRESS));
2633 need_redisplay = true;
2634 }
2635
2636 ms.wasInsideActiveEntity = true;
2637 ms.wasDown = true;
2638 }
2639
2640 }
2641 return need_redisplay;
2642
2643 }
2644
2645 const DisplayObject*
getNearestObject(const DisplayObject * o)2646 getNearestObject(const DisplayObject* o)
2647 {
2648 while (1) {
2649 assert(o);
2650 if (isReferenceable(*o)) return o;
2651 o = o->parent();
2652 }
2653 }
2654
2655 as_object*
getBuiltinObject(movie_root & mr,const ObjectURI & cl)2656 getBuiltinObject(movie_root& mr, const ObjectURI& cl)
2657 {
2658 Global_as& gl = *mr.getVM().getGlobal();
2659
2660 as_value val;
2661 if (!gl.get_member(cl, &val)) return nullptr;
2662 return toObject(val, mr.getVM());
2663 }
2664
2665 void
advanceLiveChar(MovieClip * mo)2666 advanceLiveChar(MovieClip* mo)
2667 {
2668 if (!mo->unloaded()) {
2669 #ifdef GNASH_DEBUG
2670 log_debug(" advancing DisplayObject %s", mo->getTarget());
2671 #endif
2672 mo->advance();
2673 }
2674 #ifdef GNASH_DEBUG
2675 else {
2676 log_debug(" DisplayObject %s is unloaded, not advancing it",
2677 mo->getTarget());
2678 }
2679 #endif
2680 }
2681
2682 void
notifyLoad(MovieClip * mo)2683 notifyLoad(MovieClip* mo)
2684 {
2685 if ( mo->parent() ) {
2686 mo->queueLoad();
2687 }
2688 }
2689
2690 } // anonymous namespace
2691 } // namespace gnash
2692
2693 // local Variables:
2694 // mode: C++
2695 // indent-tabs-mode: nil
2696 // End:
2697