<commit_before>define([
	"require",
  "namespace",

  // Libs
  "use!backbone",

  // Modules
  "modules/leaguevine"
],

function(require, namespace, Backbone, Leaguevine) {
    "use strict";
	var app = namespace.app;
	var GameEvent = namespace.module();
	GameEvent.Model = Backbone.Model.extend({
		defaults: {// Include defaults for any attribute that will be rendered.
			type: NaN,//Need to set a dict somewhere, probably in Leaguevine.API
			time: NaN,//YYYY-MM-DDTHH:MM:SS±hh:mm
			ordinal_number: NaN,//smaller is earlier
			game_id: NaN,
			player_1_id: NaN,
			player_2_id: NaN,
			player_3_id: NaN,
			player_1_team_id: NaN,
			player_2_team_id: NaN,
			player_3_team_id: NaN,
			int_1: NaN
		},
		urlRoot: Leaguevine.API.root + "events",
		parse: function(resp, xhr) {
			resp = Backbone.Model.prototype.parse(resp);
			return resp;
		},
		toJSON: function() {
			//TODO: Remove attributes that are not stored
			var temp = _.clone(this.attributes);
			var keys = _.keys(temp);
			_.each(keys, function(key){
				if (!temp[key]) {this.unset(key);}
			}, this);
            date = new Date(); //The time now
            this.set('time', date.toJSON().substring(0, 19) + "+00:00"); //The current time in ISO Format
			return _.clone(this.attributes);
		}
	});
	GameEvent.Collection = Backbone.Collection.extend({
		model: GameEvent.Model,
		urlRoot: Leaguevine.API.root + "events",
		url: function(models) {
			var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
			url += '/?'
			if (this.game_id) {
				url += 'game_ids=%5B' + this.game_id + '%5D&';
			}
			return url.substr(0,url.length-1);
		},
		parse: function(resp, xhr) {
			resp = Backbone.Collection.prototype.parse(resp);
			//I don't know what these will look like yet.
			return resp;
		},
		initialize: function(models, options) {
			if (options) {
				if (options.game_id) {this.game_id = options.game_id;}
    		}
		},
		comparator: function(gameevent) {// Define how items in the collection will be sorted.
		  return gameevent.get("ordinal_number");
		}
	});

	//
	// VIEWS
	//
	GameEvent.Views.Item = Backbone.View.extend({
		template: "events/item",
		tagName: "li",
		serialize: function() {return this.model.toJSON();}
	});
	GameEvent.Views.List = Backbone.View.extend({
		template: "events/list",
		className: "events-wrapper",
		render: function(layout) {
			var view = layout(this);
			//this.$el.empty()
			// call .cleanup() on all child views, and remove all appended views
			view.cleanup();
			this.collection.each(function(gameevent) {
				view.insert("ul", new Event.Views.Item({
					model: gameevent
				}));
			});
			return view.render();
		},
		initialize: function() {
			this.collection.bind("reset", function() {this.render();}, this);
		}
	});

	return GameEvent;// Required, return the module for AMD compliance
});
<commit_msg>Fixed another missing var<commit_after>define([
	"require",
  "namespace",

  // Libs
  "use!backbone",

  // Modules
  "modules/leaguevine"
],

function(require, namespace, Backbone, Leaguevine) {
    "use strict";
	var app = namespace.app;
	var GameEvent = namespace.module();
	GameEvent.Model = Backbone.Model.extend({
		defaults: {// Include defaults for any attribute that will be rendered.
			type: NaN,//Need to set a dict somewhere, probably in Leaguevine.API
			time: NaN,//YYYY-MM-DDTHH:MM:SS±hh:mm
			ordinal_number: NaN,//smaller is earlier
			game_id: NaN,
			player_1_id: NaN,
			player_2_id: NaN,
			player_3_id: NaN,
			player_1_team_id: NaN,
			player_2_team_id: NaN,
			player_3_team_id: NaN,
			int_1: NaN
		},
		urlRoot: Leaguevine.API.root + "events",
		parse: function(resp, xhr) {
			resp = Backbone.Model.prototype.parse(resp);
			return resp;
		},
		toJSON: function() {
			//TODO: Remove attributes that are not stored
			var temp = _.clone(this.attributes);
			var keys = _.keys(temp);
			_.each(keys, function(key){
				if (!temp[key]) {this.unset(key);}
			}, this);
            var date = new Date(); //The time now
            this.set('time', date.toJSON().substring(0, 19) + "+00:00"); //The current time in ISO Format
			return _.clone(this.attributes);
		}
	});
	GameEvent.Collection = Backbone.Collection.extend({
		model: GameEvent.Model,
		urlRoot: Leaguevine.API.root + "events",
		url: function(models) {
			var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
			url += '/?'
			if (this.game_id) {
				url += 'game_ids=%5B' + this.game_id + '%5D&';
			}
			return url.substr(0,url.length-1);
		},
		parse: function(resp, xhr) {
			resp = Backbone.Collection.prototype.parse(resp);
			//I don't know what these will look like yet.
			return resp;
		},
		initialize: function(models, options) {
			if (options) {
				if (options.game_id) {this.game_id = options.game_id;}
    		}
		},
		comparator: function(gameevent) {// Define how items in the collection will be sorted.
		  return gameevent.get("ordinal_number");
		}
	});

	//
	// VIEWS
	//
	GameEvent.Views.Item = Backbone.View.extend({
		template: "events/item",
		tagName: "li",
		serialize: function() {return this.model.toJSON();}
	});
	GameEvent.Views.List = Backbone.View.extend({
		template: "events/list",
		className: "events-wrapper",
		render: function(layout) {
			var view = layout(this);
			//this.$el.empty()
			// call .cleanup() on all child views, and remove all appended views
			view.cleanup();
			this.collection.each(function(gameevent) {
				view.insert("ul", new Event.Views.Item({
					model: gameevent
				}));
			});
			return view.render();
		},
		initialize: function() {
			this.collection.bind("reset", function() {this.render();}, this);
		}
	});

	return GameEvent;// Required, return the module for AMD compliance
});
<|endoftext|>


<commit_before>/**
 * @depends App.js
 * @depends Renderer.js
 * @depends ShaderProgram.js
 */

var BasicRenderer = new Class({
    Extends: Renderer,
    initialize: function(options) {
        this.parent(options);
        this.createShaderProgram();
        this.createShaders(['basic-renderer-vert', 'basic-renderer-frag']);
    },

    createShaderProgram: function() {
        this.shaderProgram = new ShaderProgram();
    },

    createShaders: function(ids) {
        for (var i = 0; i < ids; i++) {
            this.shaderProgram.addShader(ids[i]);
        }
    },

    render: function(meshes, camera) {
        this.shaderProgram.use();
        for (var i = 0; i < meshes.length; i++) {
            // Render
            this.renderMesh(meshes[i]);
        }
    },

    renderMesh: function(mesh, camera) {
        camera.modelview.pushMatrix();

        mat4.translate(camera.modelview.matrix, mesh.position);
        mat4.mult(camera.modelview.matrix, mesh.rotation);
        mat4.scale(camera.modelview.matrix, mesh.scale);

        var sp = this.shaderProgram;
        var projectionUniform = sp.getUniform('uProjectionMatrix');
        projectionUniform.setValue(camera.projection.matrix);
        var modelviewUniform = sp.getUniform('uModelviewMatrix');
        modelviewUniform.setValue(camera.modelview.matrix);

        camera.modelview.popMatrix();

        var vertexAttribute = sp.getAttribute('aVertex');
        mesh.vertexBuffer.associate(vertexAttribute);

        var colorAttribute = sp.getAttribute('aColor');
        mesh.colorBuffer.associate(colorAttribute);

        var normalAttribute = sp.getAttribute('aNormal');
        mesh.normalBuffer.associate(normalAttribute);

        gl.drawArrays(mesh.drawMode, 0, mesh.numVertices);
    }
});
<commit_msg>Fix typo<commit_after>/**
 * @depends App.js
 * @depends Renderer.js
 * @depends ShaderProgram.js
 */

var BasicRenderer = new Class({
    Extends: Renderer,
    initialize: function(options) {
        this.parent(options);
        this.createShaderProgram();
        this.createShaders(['basic-renderer-vert', 'basic-renderer-frag']);
    },

    createShaderProgram: function() {
        this.shaderProgram = new ShaderProgram();
    },

    createShaders: function(ids) {
        for (var i = 0; i < ids; i++) {
            this.shaderProgram.addShader(ids[i]);
        }
    },

    render: function(meshes, camera) {
        this.shaderProgram.use();
        for (var i = 0; i < meshes.length; i++) {
            // Render
            this.renderMesh(meshes[i]);
        }
    },

    renderMesh: function(mesh, camera) {
        camera.modelview.pushMatrix();

        mat4.translate(camera.modelview.matrix, mesh.position);
        mat4.multiply(camera.modelview.matrix, mesh.rotation);
        mat4.scale(camera.modelview.matrix, mesh.scale);

        var sp = this.shaderProgram;
        var projectionUniform = sp.getUniform('uProjectionMatrix');
        projectionUniform.setValue(camera.projection.matrix);
        var modelviewUniform = sp.getUniform('uModelviewMatrix');
        modelviewUniform.setValue(camera.modelview.matrix);

        camera.modelview.popMatrix();

        var vertexAttribute = sp.getAttribute('aVertex');
        mesh.vertexBuffer.associate(vertexAttribute);

        var colorAttribute = sp.getAttribute('aColor');
        mesh.colorBuffer.associate(colorAttribute);

        var normalAttribute = sp.getAttribute('aNormal');
        mesh.normalBuffer.associate(normalAttribute);

        gl.drawArrays(mesh.drawMode, 0, mesh.numVertices);
    }
});
<|endoftext|>


<commit_before>var map = null;
var marker = null;
var infoWindow = null;

function setMarker(latLng, map) {
    if (marker !== null) {
        marker.setMap(null);
    }

    marker = new google.maps.Marker({
        position: latLng,
        map: map,
    });

    $("#id_location_x").val(latLng.lat());
    $("#id_location_y").val(latLng.lng());
}

function initializeMap(locationLatitude, locationLongitude, zoomLevel) {
    var locationLatlng = new google.maps.LatLng(locationLatitude, locationLongitude);

    var mapOptions = {
        center: locationLatlng,
        mapTypeId: google.maps.MapTypeId.HYBRID,
        zoom: zoomLevel
    };

    var infoWindow = new google.maps.InfoWindow({
        content: ''
    });

    map = new google.maps.Map($("#map-canvas").get(0), mapOptions);
    setMarker(locationLatlng, map);

    google.maps.event.addListener(map, "click", function(event) {
        setMarker(event.latLng, map);
    });
    google.maps.event.addListener(map, "rightclick", function(event) {
        var latLng = event.latLng;

        $infoWindowContent = $("
Latitude: " + latLng.lat() + "
Longitude: " + latLng.lng() + "
" + "Place marker
");
        $infoWindowContent.find("a").click(function() {
            infoWindow.close();
            setMarker(infoWindow.getPosition(), map);
        });

        infoWindow.setContent($infoWindowContent.get(0));
        infoWindow.setPosition(event.latLng);

        infoWindow.open(map);
    });
}<commit_msg>Add minor improvements to map UI<commit_after>var map = null;
var marker = null;
var infoWindow = null;

function setMarker(latLng, map) {
    if (marker !== null) {
        marker.setMap(null);
    }

    marker = new google.maps.Marker({
        position: latLng,
        map: map,
    });

    $("#id_location_x").val(latLng.lat());
    $("#id_location_y").val(latLng.lng());
}

function initializeMap(locationLatitude, locationLongitude, zoomLevel) {
    var locationLatlng = new google.maps.LatLng(locationLatitude, locationLongitude);

    var mapOptions = {
        center: locationLatlng,
        mapTypeId: google.maps.MapTypeId.HYBRID,
        zoom: zoomLevel,
        draggableCursor: 'crosshair'
    };

    var $placeMarkerButton = $(" Place marker here");
    $placeMarkerButton.click(function() {
        infoWindow.close();
        setMarker(infoWindow.getPosition(), map);
    });

    infoWindow = new google.maps.InfoWindow({
        content: $placeMarkerButton.get(0)
    });

    map = new google.maps.Map($("#map-canvas").get(0), mapOptions);
    setMarker(locationLatlng, map);

    google.maps.event.addListener(map, "click", function(event) {
        setMarker(event.latLng, map);
    });
    google.maps.event.addListener(map, "rightclick", function(event) {
        var latLng = event.latLng;

        infoWindow.setPosition(event.latLng);

        infoWindow.open(map);
    });
}<|endoftext|>


<commit_before>/*
Copyright 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-addons-test-utils');
var expect = require('expect');
var q = require('q');
var sinon = require('sinon');

var jssdk = require('matrix-js-sdk');
var EventTimeline = jssdk.EventTimeline;

var sdk = require('matrix-react-sdk');
var TimelinePanel = sdk.getComponent('structures.TimelinePanel');
var peg = require('../../../src/MatrixClientPeg');

var test_utils = require('test-utils');

var ROOM_ID = '!room:localhost';
var USER_ID = '@me:localhost';

describe('TimelinePanel', function() {
    var sandbox;
    var room;
    var client;
    var timeline;
    var parentDiv;

    function mkMessage(opts) {
        return test_utils.mkMessage(
            {
                event: true, room: ROOM_ID, user: USER_ID,
                ts: Date.now(),
                ... opts,
            });
    }

    function scryEventTiles(panel) {
        return ReactTestUtils.scryRenderedComponentsWithType(
            panel, sdk.getComponent('rooms.EventTile'));
    };

    beforeEach(function() {
        test_utils.beforeEach(this);
        sandbox = test_utils.stubClient(sandbox);

        timeline = new jssdk.EventTimeline(ROOM_ID);
        room = sinon.createStubInstance(jssdk.Room);
        room.getLiveTimeline.returns(timeline);
        room.getPendingEvents.returns([]);

        client = peg.get();
        client.credentials = {userId: USER_ID};

        // create a div of a useful size to put our panel in, and attach it to
        // the document so that we can interact with it properly.
        parentDiv = document.createElement('div');
        parentDiv.style.width = '800px';
        parentDiv.style.height = '600px';
        parentDiv.style.overflow = 'hidden';
        document.body.appendChild(parentDiv);
    });

    afterEach(function() {
        if (parentDiv) {
            ReactDOM.unmountComponentAtNode(parentDiv);
            parentDiv.remove();
            parentDiv = null;
        }
        sandbox.restore();
    });

    it('should load new events even if you are scrolled up', function(done) {
        // this is https://github.com/vector-im/vector-web/issues/1367

        // enough events to allow us to scroll back
        var N_EVENTS = 30;
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage());
        }

        var scrollDefer;
        var panel = ReactDOM.render(
                <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                />,
                parentDiv,
        );
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when the
        // panel isn't paginating
        var awaitPaginationCompletion = function() {
            if(!panel.state.forwardPaginating)
                return q();
            else
                return q.delay(0).then(awaitPaginationCompletion);
        };

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise;
        };

        // wait for the panel to load - we'll get a scroll event once it
        // happens
        awaitScroll().then(() => {
            expect(panel.state.canBackPaginate).toBe(false);
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            // scroll up
            console.log("setting scrollTop = 0");
            scrollingDiv.scrollTop = 0;

            // wait for the scroll event to land
        }).then(awaitScroll).then(() => {
            // there should be no pagination going on now
            expect(panel.state.backPaginating).toBe(false);
            expect(panel.state.forwardPaginating).toBe(false);
            expect(panel.state.canBackPaginate).toBe(false);
            expect(panel.state.canForwardPaginate).toBe(false);
            expect(panel.isAtEndOfLiveTimeline()).toBe(false);
            expect(scrollingDiv.scrollTop).toEqual(0);

            console.log("adding event");

            // a new event!
            var ev = mkMessage();
            timeline.addEvent(ev);
            panel.onRoomTimeline(ev, room, false, false, {liveEvent: true});

            // that won't make much difference, because we don't paginate
            // unless we're at the bottom of the timeline, but a scroll event
            // should be enough to set off a pagination.
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            scrollingDiv.scrollTop = 10;
        }).delay(0).then(awaitPaginationCompletion).then(() => {
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
        }).done(done, done);
    });

    it('should not paginate forever if there are no events', function(done) {
        // start with a handful of events in the timeline, as would happen when
        // joining a room
        var d = Date.now();
        for (var i = 0; i < 3; i++) {
            timeline.addEvent(test_utils.mkMessage(
                {
                    event: true, room: ROOM_ID, user: USER_ID,
                    ts: d+i,
                }
            ));
        }
        timeline.setPaginationToken('tok', EventTimeline.BACKWARDS);

        // back-pagination returns a promise for true, but adds no events
        client.paginateEventTimeline = sinon.spy((tl, opts) => {
            console.log("paginate:", opts);
            expect(opts.backwards).toBe(true);
            return q(true);
        });

        var panel = ReactDOM.render(
            <TimelinePanel room={room}/>,
            parentDiv
        );

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));

        expect(messagePanel.props.backPaginating).toBe(true);

        // let the first round of pagination finish off
        setTimeout(() => {
            // at this point, the timeline window should have tried to paginate
            // 5 times, and we should have given up paginating
            expect(client.paginateEventTimeline.callCount).toEqual(5);
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);

            // now, if we update the events, there shouldn't be any
            // more requests.
            client.paginateEventTimeline.reset();
            panel.forceUpdate();
            expect(messagePanel.props.backPaginating).toBe(false);
            setTimeout(() => {
                expect(client.paginateEventTimeline.callCount).toEqual(0);
                done();
            }, 0);
        }, 0);
    });

    it("should let you scroll down again after you've scrolled up", function(done) {
        var TIMELINE_CAP = 100; // needs to be more than we can fit in the div
        var N_EVENTS = 120;     // needs to be more than TIMELINE_CAP

        // sadly, loading all those events takes a while
        this.timeout(N_EVENTS * 50);

        // client.getRoom is called a /lot/ in this test, so replace
        // sinon's spy with a fast noop.
        client.getRoom = function(id) { return null; };

        // fill the timeline with lots of events
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage({msg: ""+i}));
        }
        console.log("added events to timeline");

        var scrollDefer;
        var panel = ReactDOM.render(
            <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                timelineCap={TIMELINE_CAP}
            />,
            parentDiv
        );
        console.log("TimelinePanel rendered");

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise.then(() => {
                console.log("got scroll event; scrollTop now " +
                            scrollingDiv.scrollTop);
            });
        };

        function setScrollTop(scrollTop) {
            const before = scrollingDiv.scrollTop;
            scrollingDiv.scrollTop = scrollTop;
            console.log("setScrollTop: before update: " + before +
                        "; assigned: " + scrollTop +
                        "; after update: " + scrollingDiv.scrollTop);
        }

        function backPaginate() {
            console.log("back paginating...");
            setScrollTop(0);
            return awaitScroll().then(() => {
                let eventTiles = scryEventTiles(panel);
                let firstEvent = eventTiles[0].props.mxEvent;

                console.log("TimelinePanel contains " + eventTiles.length +
                            " events; first is " +
                            firstEvent.getContent().body);

                if(scrollingDiv.scrollTop > 0) {
                    // need to go further
                    return backPaginate();
                }
                console.log("paginated to start.");
            });
        }

        // let the first round of pagination finish off
        awaitScroll().then(() => {
            // we should now have loaded the first few events
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            // back-paginate until we hit the start
            return backPaginate();
        }).then(() => {
            // hopefully, we got to the start of the timeline
            expect(messagePanel.props.backPaginating).toBe(false);

            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
            var events = scryEventTiles(panel);
            expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we should now be able to scroll down, and paginate in the other
            // direction.
            setScrollTop(scrollingDiv.scrollHeight);
            scrollingDiv.scrollTop = scrollingDiv.scrollHeight;

            // the delay() below is a heinous hack to deal with the fact that,
            // without it, we may or may not get control back before the
            // forward pagination completes. The delay means that it should
            // have completed.
            return awaitScroll().delay(0);
        }).then(() => {
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.forwardPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            var events = scryEventTiles(panel);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we don't really know what the first event tile will be, since that
            // depends on how much the timelinepanel decides to paginate.
            //
            // just check that the first tile isn't event 0.
            expect(events[0].props.mxEvent).toNotBe(timeline.getEvents()[0]);

            console.log("done");
        }).done(done, done);
    });
});
<commit_msg>Add text prefix to test event content<commit_after>/*
Copyright 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-addons-test-utils');
var expect = require('expect');
var q = require('q');
var sinon = require('sinon');

var jssdk = require('matrix-js-sdk');
var EventTimeline = jssdk.EventTimeline;

var sdk = require('matrix-react-sdk');
var TimelinePanel = sdk.getComponent('structures.TimelinePanel');
var peg = require('../../../src/MatrixClientPeg');

var test_utils = require('test-utils');

var ROOM_ID = '!room:localhost';
var USER_ID = '@me:localhost';

describe('TimelinePanel', function() {
    var sandbox;
    var room;
    var client;
    var timeline;
    var parentDiv;

    function mkMessage(opts) {
        return test_utils.mkMessage(
            {
                event: true, room: ROOM_ID, user: USER_ID,
                ts: Date.now(),
                ... opts,
            });
    }

    function scryEventTiles(panel) {
        return ReactTestUtils.scryRenderedComponentsWithType(
            panel, sdk.getComponent('rooms.EventTile'));
    };

    beforeEach(function() {
        test_utils.beforeEach(this);
        sandbox = test_utils.stubClient(sandbox);

        timeline = new jssdk.EventTimeline(ROOM_ID);
        room = sinon.createStubInstance(jssdk.Room);
        room.getLiveTimeline.returns(timeline);
        room.getPendingEvents.returns([]);

        client = peg.get();
        client.credentials = {userId: USER_ID};

        // create a div of a useful size to put our panel in, and attach it to
        // the document so that we can interact with it properly.
        parentDiv = document.createElement('div');
        parentDiv.style.width = '800px';
        parentDiv.style.height = '600px';
        parentDiv.style.overflow = 'hidden';
        document.body.appendChild(parentDiv);
    });

    afterEach(function() {
        if (parentDiv) {
            ReactDOM.unmountComponentAtNode(parentDiv);
            parentDiv.remove();
            parentDiv = null;
        }
        sandbox.restore();
    });

    it('should load new events even if you are scrolled up', function(done) {
        // this is https://github.com/vector-im/vector-web/issues/1367

        // enough events to allow us to scroll back
        var N_EVENTS = 30;
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage());
        }

        var scrollDefer;
        var panel = ReactDOM.render(
                <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                />,
                parentDiv,
        );
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when the
        // panel isn't paginating
        var awaitPaginationCompletion = function() {
            if(!panel.state.forwardPaginating)
                return q();
            else
                return q.delay(0).then(awaitPaginationCompletion);
        };

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise;
        };

        // wait for the panel to load - we'll get a scroll event once it
        // happens
        awaitScroll().then(() => {
            expect(panel.state.canBackPaginate).toBe(false);
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            // scroll up
            console.log("setting scrollTop = 0");
            scrollingDiv.scrollTop = 0;

            // wait for the scroll event to land
        }).then(awaitScroll).then(() => {
            // there should be no pagination going on now
            expect(panel.state.backPaginating).toBe(false);
            expect(panel.state.forwardPaginating).toBe(false);
            expect(panel.state.canBackPaginate).toBe(false);
            expect(panel.state.canForwardPaginate).toBe(false);
            expect(panel.isAtEndOfLiveTimeline()).toBe(false);
            expect(scrollingDiv.scrollTop).toEqual(0);

            console.log("adding event");

            // a new event!
            var ev = mkMessage();
            timeline.addEvent(ev);
            panel.onRoomTimeline(ev, room, false, false, {liveEvent: true});

            // that won't make much difference, because we don't paginate
            // unless we're at the bottom of the timeline, but a scroll event
            // should be enough to set off a pagination.
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            scrollingDiv.scrollTop = 10;
        }).delay(0).then(awaitPaginationCompletion).then(() => {
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
        }).done(done, done);
    });

    it('should not paginate forever if there are no events', function(done) {
        // start with a handful of events in the timeline, as would happen when
        // joining a room
        var d = Date.now();
        for (var i = 0; i < 3; i++) {
            timeline.addEvent(test_utils.mkMessage(
                {
                    event: true, room: ROOM_ID, user: USER_ID,
                    ts: d+i,
                }
            ));
        }
        timeline.setPaginationToken('tok', EventTimeline.BACKWARDS);

        // back-pagination returns a promise for true, but adds no events
        client.paginateEventTimeline = sinon.spy((tl, opts) => {
            console.log("paginate:", opts);
            expect(opts.backwards).toBe(true);
            return q(true);
        });

        var panel = ReactDOM.render(
            <TimelinePanel room={room}/>,
            parentDiv
        );

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));

        expect(messagePanel.props.backPaginating).toBe(true);

        // let the first round of pagination finish off
        setTimeout(() => {
            // at this point, the timeline window should have tried to paginate
            // 5 times, and we should have given up paginating
            expect(client.paginateEventTimeline.callCount).toEqual(5);
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);

            // now, if we update the events, there shouldn't be any
            // more requests.
            client.paginateEventTimeline.reset();
            panel.forceUpdate();
            expect(messagePanel.props.backPaginating).toBe(false);
            setTimeout(() => {
                expect(client.paginateEventTimeline.callCount).toEqual(0);
                done();
            }, 0);
        }, 0);
    });

    it("should let you scroll down again after you've scrolled up", function(done) {
        var TIMELINE_CAP = 100; // needs to be more than we can fit in the div
        var N_EVENTS = 120;     // needs to be more than TIMELINE_CAP

        // sadly, loading all those events takes a while
        this.timeout(N_EVENTS * 50);

        // client.getRoom is called a /lot/ in this test, so replace
        // sinon's spy with a fast noop.
        client.getRoom = function(id) { return null; };

        // fill the timeline with lots of events
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage({msg: "Event "+i}));
        }
        console.log("added events to timeline");

        var scrollDefer;
        var panel = ReactDOM.render(
            <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                timelineCap={TIMELINE_CAP}
            />,
            parentDiv
        );
        console.log("TimelinePanel rendered");

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise.then(() => {
                console.log("got scroll event; scrollTop now " +
                            scrollingDiv.scrollTop);
            });
        };

        function setScrollTop(scrollTop) {
            const before = scrollingDiv.scrollTop;
            scrollingDiv.scrollTop = scrollTop;
            console.log("setScrollTop: before update: " + before +
                        "; assigned: " + scrollTop +
                        "; after update: " + scrollingDiv.scrollTop);
        }

        function backPaginate() {
            console.log("back paginating...");
            setScrollTop(0);
            return awaitScroll().then(() => {
                let eventTiles = scryEventTiles(panel);
                let firstEvent = eventTiles[0].props.mxEvent;

                console.log("TimelinePanel contains " + eventTiles.length +
                            " events; first is " +
                            firstEvent.getContent().body);

                if(scrollingDiv.scrollTop > 0) {
                    // need to go further
                    return backPaginate();
                }
                console.log("paginated to start.");
            });
        }

        // let the first round of pagination finish off
        awaitScroll().then(() => {
            // we should now have loaded the first few events
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            // back-paginate until we hit the start
            return backPaginate();
        }).then(() => {
            // hopefully, we got to the start of the timeline
            expect(messagePanel.props.backPaginating).toBe(false);

            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
            var events = scryEventTiles(panel);
            expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we should now be able to scroll down, and paginate in the other
            // direction.
            setScrollTop(scrollingDiv.scrollHeight);
            scrollingDiv.scrollTop = scrollingDiv.scrollHeight;

            // the delay() below is a heinous hack to deal with the fact that,
            // without it, we may or may not get control back before the
            // forward pagination completes. The delay means that it should
            // have completed.
            return awaitScroll().delay(0);
        }).then(() => {
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.forwardPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            var events = scryEventTiles(panel);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we don't really know what the first event tile will be, since that
            // depends on how much the timelinepanel decides to paginate.
            //
            // just check that the first tile isn't event 0.
            expect(events[0].props.mxEvent).toNotBe(timeline.getEvents()[0]);

            console.log("done");
        }).done(done, done);
    });
});
<|endoftext|>


<commit_before>var websiteNotFound = {
	name: "Server not found",
	message: "Lulumi-browser couldn't find this website. If you think there should be something here, you can search on archive.org for this page."
}

var sslError = {
	name: "This website is not available",
	message: "Lulumi-browser couldn't connect securely to this website."
}

var dnsError = {
	name: "Website not found",
	messge: "A DNS error occured."
}

var offlineError = {
	name: "You are offline",
	message: "Reconnect to the internet and try again."
}

//list: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h&sq=package:chromium&type=cs
const errorCodes = {
	"-21": offlineError, //network changed
	"-104": {
		message: "Min couldn't connect to the website."
	},
	"-105": websiteNotFound,
	"-106": offlineError,
	"-107": sslError,
	"-109": websiteNotFound,
	"-110": sslError, //this is actually the error code for "server requested a client certificate", but we don't support that yet,
	"-112": sslError,
	"-113": sslError,
	"-116": sslError,
	"-117": sslError,
	"-200": sslError,
	"-201": {
		name: "This website is not available",
		message: "Min couldn't connect securely to this website. Please make sure your computer's clock is set correctly."
	},
	"-202": sslError,
	"-203": sslError,
	"-204": sslError,
	"-205": sslError,
	"-206": sslError,
	"-207": sslError,
	"-208": sslError,
	"-210": sslError,
	"-211": sslError,
	"-212": sslError,
	"-213": sslError,
	"-300": {
		name: "This address is invalid."
	},
	"-501": sslError,
	"-800": dnsError,
	"-801": dnsError,
	"-802": dnsError,
	"-803": dnsError,
	"-804": dnsError,
	"-805": dnsError,
	"-806": dnsError,
}

var searchParams = new URLSearchParams(window.location.search.replace("?", ""));

//show the error message and detail

var h1 = document.getElementById("error-name");
var h2 = document.getElementById("error-desc");
var button = document.getElementById("ta-button");

var ec = searchParams.get("ec");
var url = searchParams.get("url");

if (errorCodes[ec]) {
	h1.innerHTML += errorCodes[ec].name || "";
	h2.innerHTML += errorCodes[ec].message || "";
} else {
	h1.innerHTML += "An error occured";
}

var link = document.getElementById("archivesearch-link");

if (link) {
	link.href = "https://web.archive.org/web/*/" + url;
}

//if an ssl error occured, "try again" should go to the http:// version, which might work

if (errorCodes[ec] == sslError) {
	url = url.replace("https://", "http://");
}

if (url) {
	button.addEventListener("click", function () {
		window.location = url;
	});
}
<commit_msg>fix lint errors<commit_after>const websiteNotFound = {
  name: 'Server not found',
  /* eslint-disable max-len */
  message: "Lulumi-browser couldn't find this website. If you think there should be something here, you can search on archive.org for this page.",
};

const sslError = {
  name: 'This website is not available',
  message: "Lulumi-browser couldn't connect securely to this website.",
};

const dnsError = {
  name: 'Website not found',
  messge: 'A DNS error occured.',
};

const offlineError = {
  name: 'You are offline',
  message: 'Reconnect to the internet and try again.',
};

// list: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h&sq=package:chromium&type=cs
const errorCodes = {
  '-21': offlineError, // network changed
  '-104': {
    message: "Min couldn't connect to the website.",
  },
  '-105': websiteNotFound,
  '-106': offlineError,
  '-107': sslError,
  '-109': websiteNotFound,
  /* eslint-disable max-len */
  '-110': sslError, // this is actually the error code for "server requested a client certificate", but we don't support that yet,
  '-112': sslError,
  '-113': sslError,
  '-116': sslError,
  '-117': sslError,
  '-200': sslError,
  '-201': {
    name: 'This website is not available',
    /* eslint-disable max-len */
    message: "Lulumi-browser couldn't connect securely to this website. Please make sure your computer's clock is set correctly.",
  },
  '-202': sslError,
  '-203': sslError,
  '-204': sslError,
  '-205': sslError,
  '-206': sslError,
  '-207': sslError,
  '-208': sslError,
  '-210': sslError,
  '-211': sslError,
  '-212': sslError,
  '-213': sslError,
  '-300': {
    name: 'This address is invalid.',
  },
  '-501': sslError,
  '-800': dnsError,
  '-801': dnsError,
  '-802': dnsError,
  '-803': dnsError,
  '-804': dnsError,
  '-805': dnsError,
  '-806': dnsError,
};

const searchParams = new URLSearchParams(window.location.search.replace('?', ''));

// show the error message and detail

const h1 = document.getElementById('error-name');
const h2 = document.getElementById('error-desc');
const button = document.getElementById('ta-button');

const ec = searchParams.get('ec');
let url = searchParams.get('url');

if (errorCodes[ec]) {
  h1.innerHTML += errorCodes[ec].name || '';
  h2.innerHTML += errorCodes[ec].message || '';
} else {
  h1.innerHTML += 'An error occured';
}

const link = document.getElementById('archivesearch-link');

if (link) {
  link.href = `https://web.archive.org/web/*/${url}`;
}

// if an ssl error occured, "try again" should go to the http:// version, which might work

if (errorCodes[ec] === sslError) {
  url = url.replace('https://', 'http://');
}

if (url) {
  button.addEventListener('click', () => {
    window.location = url;
  });
}
<|endoftext|>


<commit_before>/*
Copyright 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-addons-test-utils');
var expect = require('expect');
var q = require('q');
var sinon = require('sinon');

var jssdk = require('matrix-js-sdk');
var EventTimeline = jssdk.EventTimeline;

var sdk = require('matrix-react-sdk');
var TimelinePanel = sdk.getComponent('structures.TimelinePanel');
var peg = require('../../../src/MatrixClientPeg');

var test_utils = require('test-utils');

var ROOM_ID = '!room:localhost';
var USER_ID = '@me:localhost';

describe('TimelinePanel', function() {
    var sandbox;
    var room;
    var client;
    var timeline;
    var parentDiv;

    function mkMessage(opts) {
        return test_utils.mkMessage(
            {
                event: true, room: ROOM_ID, user: USER_ID,
                ts: Date.now(),
                ... opts,
            });
    }

    function scryEventTiles(panel) {
        return ReactTestUtils.scryRenderedComponentsWithType(
            panel, sdk.getComponent('rooms.EventTile'));
    };

    beforeEach(function() {
        test_utils.beforeEach(this);
        sandbox = test_utils.stubClient(sandbox);

        timeline = new jssdk.EventTimeline(ROOM_ID);
        room = sinon.createStubInstance(jssdk.Room);
        room.getLiveTimeline.returns(timeline);
        room.getPendingEvents.returns([]);

        client = peg.get();
        client.credentials = {userId: USER_ID};

        // create a div of a useful size to put our panel in, and attach it to
        // the document so that we can interact with it properly.
        parentDiv = document.createElement('div');
        parentDiv.style.width = '800px';
        parentDiv.style.height = '600px';
        parentDiv.style.overflow = 'hidden';
        document.body.appendChild(parentDiv);
    });

    afterEach(function() {
        if (parentDiv) {
            ReactDOM.unmountComponentAtNode(parentDiv);
            parentDiv.remove();
            parentDiv = null;
        }
        sandbox.restore();
    });

    it('should load new events even if you are scrolled up', function(done) {
        // this is https://github.com/vector-im/vector-web/issues/1367

        // enough events to allow us to scroll back
        var N_EVENTS = 30;
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage());
        }

        var scrollDefer;
        var panel = ReactDOM.render(
                <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                />,
                parentDiv,
        );
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when the
        // panel isn't paginating
        var awaitPaginationCompletion = function() {
            if(!panel.state.forwardPaginating)
                return q();
            else
                return q.delay(0).then(awaitPaginationCompletion);
        };

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise;
        };

        // wait for the panel to load - we'll get a scroll event once it
        // happens
        awaitScroll().then(() => {
            expect(panel.state.canBackPaginate).toBe(false);
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            // scroll up
            console.log("setting scrollTop = 0");
            scrollingDiv.scrollTop = 0;

            // wait for the scroll event to land
        }).then(awaitScroll).then(() => {
            // there should be no pagination going on now
            expect(panel.state.backPaginating).toBe(false);
            expect(panel.state.forwardPaginating).toBe(false);
            expect(panel.state.canBackPaginate).toBe(false);
            expect(panel.state.canForwardPaginate).toBe(false);
            expect(panel.isAtEndOfLiveTimeline()).toBe(false);
            expect(scrollingDiv.scrollTop).toEqual(0);

            console.log("adding event");

            // a new event!
            var ev = mkMessage();
            timeline.addEvent(ev);
            panel.onRoomTimeline(ev, room, false, false, {liveEvent: true});

            // that won't make much difference, because we don't paginate
            // unless we're at the bottom of the timeline, but a scroll event
            // should be enough to set off a pagination.
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            scrollingDiv.scrollTop = 10;
        }).delay(0).then(awaitPaginationCompletion).then(() => {
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
        }).done(done, done);
    });

    it('should not paginate forever if there are no events', function(done) {
        // start with a handful of events in the timeline, as would happen when
        // joining a room
        var d = Date.now();
        for (var i = 0; i < 3; i++) {
            timeline.addEvent(test_utils.mkMessage(
                {
                    event: true, room: ROOM_ID, user: USER_ID,
                    ts: d+i,
                }
            ));
        }
        timeline.setPaginationToken('tok', EventTimeline.BACKWARDS);

        // back-pagination returns a promise for true, but adds no events
        client.paginateEventTimeline = sinon.spy((tl, opts) => {
            console.log("paginate:", opts);
            expect(opts.backwards).toBe(true);
            return q(true);
        });

        var panel = ReactDOM.render(
            <TimelinePanel room={room}/>,
            parentDiv
        );

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));

        expect(messagePanel.props.backPaginating).toBe(true);

        // let the first round of pagination finish off
        setTimeout(() => {
            // at this point, the timeline window should have tried to paginate
            // 5 times, and we should have given up paginating
            expect(client.paginateEventTimeline.callCount).toEqual(5);
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);

            // now, if we update the events, there shouldn't be any
            // more requests.
            client.paginateEventTimeline.reset();
            panel.forceUpdate();
            expect(messagePanel.props.backPaginating).toBe(false);
            setTimeout(() => {
                expect(client.paginateEventTimeline.callCount).toEqual(0);
                done();
            }, 0);
        }, 0);
    });

    it("should let you scroll down again after you've scrolled up", function(done) {
        var TIMELINE_CAP = 100; // needs to be more than we can fit in the div
        var N_EVENTS = 120;     // needs to be more than TIMELINE_CAP

        // sadly, loading all those events takes a while
        this.timeout(N_EVENTS * 50);

        // client.getRoom is called a /lot/ in this test, so replace
        // sinon's spy with a fast noop.
        client.getRoom = function(id) { return null; };

        // fill the timeline with lots of events
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage({msg: ""+i}));
        }
        console.log("added events to timeline");

        var scrollDefer;
        var panel = ReactDOM.render(
            <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                timelineCap={TIMELINE_CAP}
            />,
            parentDiv
        );
        console.log("TimelinePanel rendered");

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise.then(() => {
                console.log("got scroll event; scrollTop now " +
                            scrollingDiv.scrollTop);
            });
        };

        function setScrollTop(scrollTop) {
            const before = scrollingDiv.scrollTop;
            scrollingDiv.scrollTop = scrollTop;
            console.log("setScrollTop: before update: " + before +
                        "; assigned: " + scrollTop +
                        "; after update: " + scrollingDiv.scrollTop);
        }

        function backPaginate() {
            console.log("back paginating...");
            setScrollTop(0);
            return awaitScroll().then(() => {
                let eventTiles = scryEventTiles(panel);
                let firstEvent = eventTiles[0].props.mxEvent;

                console.log("TimelinePanel contains " + eventTiles.length +
                            " events; first is " +
                            firstEvent.getContent().body);

                if(scrollingDiv.scrollTop > 0) {
                    // need to go further
                    return backPaginate();
                }
                console.log("paginated to start.");
            });
        }

        // let the first round of pagination finish off
        awaitScroll().then(() => {
            // we should now have loaded the first few events
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            // back-paginate until we hit the start
            return backPaginate();
        }).then(() => {
            // hopefully, we got to the start of the timeline
            expect(messagePanel.props.backPaginating).toBe(false);

            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
            var events = scryEventTiles(panel);
            expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we should now be able to scroll down, and paginate in the other
            // direction.
            setScrollTop(scrollingDiv.scrollHeight);
            scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
            return awaitScroll();
        }).then(() => {
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.forwardPaginating).toBe(true);
            return awaitScroll();
        }).then(() => {
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.forwardPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            var events = scryEventTiles(panel);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we don't really know what the first event tile will be, since that
            // depends on how much the timelinepanel decides to paginate.
            //
            // just check that the first tile isn't event 0.
            expect(events[0].props.mxEvent).toNotBe(timeline.getEvents()[0]);

            console.log("done");
        }).done(done, done);
    });
});
<commit_msg>TimelinePanel-test: Hackery to make it pass<commit_after>/*
Copyright 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-addons-test-utils');
var expect = require('expect');
var q = require('q');
var sinon = require('sinon');

var jssdk = require('matrix-js-sdk');
var EventTimeline = jssdk.EventTimeline;

var sdk = require('matrix-react-sdk');
var TimelinePanel = sdk.getComponent('structures.TimelinePanel');
var peg = require('../../../src/MatrixClientPeg');

var test_utils = require('test-utils');

var ROOM_ID = '!room:localhost';
var USER_ID = '@me:localhost';

describe('TimelinePanel', function() {
    var sandbox;
    var room;
    var client;
    var timeline;
    var parentDiv;

    function mkMessage(opts) {
        return test_utils.mkMessage(
            {
                event: true, room: ROOM_ID, user: USER_ID,
                ts: Date.now(),
                ... opts,
            });
    }

    function scryEventTiles(panel) {
        return ReactTestUtils.scryRenderedComponentsWithType(
            panel, sdk.getComponent('rooms.EventTile'));
    };

    beforeEach(function() {
        test_utils.beforeEach(this);
        sandbox = test_utils.stubClient(sandbox);

        timeline = new jssdk.EventTimeline(ROOM_ID);
        room = sinon.createStubInstance(jssdk.Room);
        room.getLiveTimeline.returns(timeline);
        room.getPendingEvents.returns([]);

        client = peg.get();
        client.credentials = {userId: USER_ID};

        // create a div of a useful size to put our panel in, and attach it to
        // the document so that we can interact with it properly.
        parentDiv = document.createElement('div');
        parentDiv.style.width = '800px';
        parentDiv.style.height = '600px';
        parentDiv.style.overflow = 'hidden';
        document.body.appendChild(parentDiv);
    });

    afterEach(function() {
        if (parentDiv) {
            ReactDOM.unmountComponentAtNode(parentDiv);
            parentDiv.remove();
            parentDiv = null;
        }
        sandbox.restore();
    });

    it('should load new events even if you are scrolled up', function(done) {
        // this is https://github.com/vector-im/vector-web/issues/1367

        // enough events to allow us to scroll back
        var N_EVENTS = 30;
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage());
        }

        var scrollDefer;
        var panel = ReactDOM.render(
                <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                />,
                parentDiv,
        );
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when the
        // panel isn't paginating
        var awaitPaginationCompletion = function() {
            if(!panel.state.forwardPaginating)
                return q();
            else
                return q.delay(0).then(awaitPaginationCompletion);
        };

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise;
        };

        // wait for the panel to load - we'll get a scroll event once it
        // happens
        awaitScroll().then(() => {
            expect(panel.state.canBackPaginate).toBe(false);
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            // scroll up
            console.log("setting scrollTop = 0");
            scrollingDiv.scrollTop = 0;

            // wait for the scroll event to land
        }).then(awaitScroll).then(() => {
            // there should be no pagination going on now
            expect(panel.state.backPaginating).toBe(false);
            expect(panel.state.forwardPaginating).toBe(false);
            expect(panel.state.canBackPaginate).toBe(false);
            expect(panel.state.canForwardPaginate).toBe(false);
            expect(panel.isAtEndOfLiveTimeline()).toBe(false);
            expect(scrollingDiv.scrollTop).toEqual(0);

            console.log("adding event");

            // a new event!
            var ev = mkMessage();
            timeline.addEvent(ev);
            panel.onRoomTimeline(ev, room, false, false, {liveEvent: true});

            // that won't make much difference, because we don't paginate
            // unless we're at the bottom of the timeline, but a scroll event
            // should be enough to set off a pagination.
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);

            scrollingDiv.scrollTop = 10;
        }).delay(0).then(awaitPaginationCompletion).then(() => {
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
        }).done(done, done);
    });

    it('should not paginate forever if there are no events', function(done) {
        // start with a handful of events in the timeline, as would happen when
        // joining a room
        var d = Date.now();
        for (var i = 0; i < 3; i++) {
            timeline.addEvent(test_utils.mkMessage(
                {
                    event: true, room: ROOM_ID, user: USER_ID,
                    ts: d+i,
                }
            ));
        }
        timeline.setPaginationToken('tok', EventTimeline.BACKWARDS);

        // back-pagination returns a promise for true, but adds no events
        client.paginateEventTimeline = sinon.spy((tl, opts) => {
            console.log("paginate:", opts);
            expect(opts.backwards).toBe(true);
            return q(true);
        });

        var panel = ReactDOM.render(
            <TimelinePanel room={room}/>,
            parentDiv
        );

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));

        expect(messagePanel.props.backPaginating).toBe(true);

        // let the first round of pagination finish off
        setTimeout(() => {
            // at this point, the timeline window should have tried to paginate
            // 5 times, and we should have given up paginating
            expect(client.paginateEventTimeline.callCount).toEqual(5);
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);

            // now, if we update the events, there shouldn't be any
            // more requests.
            client.paginateEventTimeline.reset();
            panel.forceUpdate();
            expect(messagePanel.props.backPaginating).toBe(false);
            setTimeout(() => {
                expect(client.paginateEventTimeline.callCount).toEqual(0);
                done();
            }, 0);
        }, 0);
    });

    it("should let you scroll down again after you've scrolled up", function(done) {
        var TIMELINE_CAP = 100; // needs to be more than we can fit in the div
        var N_EVENTS = 120;     // needs to be more than TIMELINE_CAP

        // sadly, loading all those events takes a while
        this.timeout(N_EVENTS * 50);

        // client.getRoom is called a /lot/ in this test, so replace
        // sinon's spy with a fast noop.
        client.getRoom = function(id) { return null; };

        // fill the timeline with lots of events
        for (var i = 0; i < N_EVENTS; i++) {
            timeline.addEvent(mkMessage({msg: ""+i}));
        }
        console.log("added events to timeline");

        var scrollDefer;
        var panel = ReactDOM.render(
            <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
                timelineCap={TIMELINE_CAP}
            />,
            parentDiv
        );
        console.log("TimelinePanel rendered");

        var messagePanel = ReactTestUtils.findRenderedComponentWithType(
            panel, sdk.getComponent('structures.MessagePanel'));
        var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
            panel, "gm-scroll-view");

        // helper function which will return a promise which resolves when
        // the TimelinePanel fires a scroll event
        var awaitScroll = function() {
            scrollDefer = q.defer();
            return scrollDefer.promise.then(() => {
                console.log("got scroll event; scrollTop now " +
                            scrollingDiv.scrollTop);
            });
        };

        function setScrollTop(scrollTop) {
            const before = scrollingDiv.scrollTop;
            scrollingDiv.scrollTop = scrollTop;
            console.log("setScrollTop: before update: " + before +
                        "; assigned: " + scrollTop +
                        "; after update: " + scrollingDiv.scrollTop);
        }

        function backPaginate() {
            console.log("back paginating...");
            setScrollTop(0);
            return awaitScroll().then(() => {
                let eventTiles = scryEventTiles(panel);
                let firstEvent = eventTiles[0].props.mxEvent;

                console.log("TimelinePanel contains " + eventTiles.length +
                            " events; first is " +
                            firstEvent.getContent().body);

                if(scrollingDiv.scrollTop > 0) {
                    // need to go further
                    return backPaginate();
                }
                console.log("paginated to start.");
            });
        }

        // let the first round of pagination finish off
        awaitScroll().then(() => {
            // we should now have loaded the first few events
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            // back-paginate until we hit the start
            return backPaginate();
        }).then(() => {
            // hopefully, we got to the start of the timeline
            expect(messagePanel.props.backPaginating).toBe(false);

            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
            var events = scryEventTiles(panel);
            expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we should now be able to scroll down, and paginate in the other
            // direction.
            setScrollTop(scrollingDiv.scrollHeight);
            scrollingDiv.scrollTop = scrollingDiv.scrollHeight;

            // the delay() below is a heinous hack to deal with the fact that,
            // without it, we may or may not get control back before the
            // forward pagination completes. The delay means that it should
            // have completed.
            return awaitScroll().delay(0);
        }).then(() => {
            expect(messagePanel.props.backPaginating).toBe(false);
            expect(messagePanel.props.forwardPaginating).toBe(false);
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);

            var events = scryEventTiles(panel);
            expect(events.length).toEqual(TIMELINE_CAP);

            // we don't really know what the first event tile will be, since that
            // depends on how much the timelinepanel decides to paginate.
            //
            // just check that the first tile isn't event 0.
            expect(events[0].props.mxEvent).toNotBe(timeline.getEvents()[0]);

            console.log("done");
        }).done(done, done);
    });
});
<|endoftext|>


<commit_before>/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2015, Kyle Kelley and others as credited in the AUTHORS file
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

/**
 * @module jupyter-paths
 *
 * @description Module `jupyter-paths` provides path helpers ported from github.com/jupyter/jupyter_core
 */

var fs = require('fs');
var path = require('path');
var child_process = require('child_process');

function JupyterPaths() {
  var pathsResponse, basePaths;

  try {
    pathsResponse = child_process.spawnSync('jupyter',  ['--paths', '--json']);
    basePaths = JSON.parse(pathsResponse.stdout.toString());
  } catch(e) {
    console.error(e);
    console.error(pathsResponse.stdout.toString());
    console.error(pathsResponse.stderr.toString());
    return;
  }

  this.basePaths = basePaths;
}

// Get the Jupyter config directory for this platform and user.
JupyterPaths.prototype.configDirs = function () {
  return this.basePaths.config;
};

/**
 * Get the config directory for Jupyter data files.
 * These are non-transient, non-configuration files.
 */
JupyterPaths.prototype.dataDirs = function() {
  return this.basePaths.data;
};

// Return the runtime dirs for transient jupyter files.
JupyterPaths.prototype.runtimeDirs = function() {
  return this.basePaths.runtime;
};

JupyterPaths.prototype.kernelDirs = function() {
  var dataDirs = this.dataDirs();

  return dataDirs.map(function(p){
    return path.join(p, "kernels");
  });
};

module.exports = new JupyterPaths();
<commit_msg>Simplify this.path<commit_after>/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2015, Kyle Kelley and others as credited in the AUTHORS file
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

/**
 * @module jupyter-paths
 *
 * @description Module `jupyter-paths` provides path helpers ported from github.com/jupyter/jupyter_core
 */

var fs = require('fs');
var path = require('path');
var child_process = require('child_process');

function JupyterPaths() {
  var pathsResponse, path;

  try {
    pathsResponse = child_process.spawnSync('jupyter',  ['--paths', '--json']);
    _path = JSON.parse(pathsResponse.stdout.toString());
  } catch(e) {
    console.error(e);
    console.error(pathsResponse.stdout.toString());
    console.error(pathsResponse.stderr.toString());
    return;
  }

  this.path = _path;
}

// Get the Jupyter config directory for this platform and user.
JupyterPaths.prototype.configDirs = function () {
  return this.path.config;
};

/**
 * Get the config directory for Jupyter data files.
 * These are non-transient, non-configuration files.
 */
JupyterPaths.prototype.dataDirs = function() {
  return this.path.data;
};

// Return the runtime dirs for transient jupyter files.
JupyterPaths.prototype.runtimeDirs = function() {
  return this.path.runtime;
};

JupyterPaths.prototype.kernelDirs = function() {
  var dataDirs = this.dataDirs();

  return dataDirs.map(function(p){
    return path.join(p, "kernels");
  });
};

module.exports = new JupyterPaths();
<|endoftext|>


<commit_before>/**
 * Copyright 2019 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dict, map} from '../utils/object';
import {isAmp4Email} from '../format';
import {isUrlAttribute} from '../url-rewrite';
import {startsWith} from '../string';

/** @const {string} */
export const BIND_PREFIX = 'data-amp-bind-';

/** @const {string} */
export const DIFF_KEY = 'i-amphtml-key';

/** @const {string} */
export const DIFF_IGNORE = 'i-amphtml-ignore';

/**
 * Map of AMP element tag name to attributes that, if changed, require
 * replacement of the original element.
 * @const {!Object<string, !Array<string>>}
 */
export const DIFFABLE_AMP_ELEMENTS = {
  'AMP-IMG': ['src', 'srcset', 'layout', 'width', 'height'],
};

/**
 * Most AMP elements don't support ad hoc mutation and should be replaced
 * instead of DOM diff'ed. Some AMP elements can be manually diff'ed.
 *
 * Both of these cases require a special attribute to enable special handling in
 * the diffing algorithm. This function sets the appropriate attribute.
 *
 * @param {!Element} element
 * @param {function(): string} generateKey
 */
export function markElementForDiffing(element, generateKey) {
  const isAmpElement = startsWith(element.tagName, 'AMP-');
  // Don't DOM diff nodes with bindings because amp-bind scans newly rendered
  // elements and discards _all_ old elements _before_ diffing, so preserving
  // old elements would cause loss of functionality.
  //
  // Alternatively, we could do diffing _before_ bindings are updated and
  // more precisely add/remove bindings from nodes that set-dom diffs, but...
  // 1. Would result in content flashing unless we clone the old DOM tree.
  // 2. Need to change set-dom e.g. handle changed bind attribute values.
  const hasBinding = element.hasAttribute('i-amphtml-binding');

  if (!hasBinding && DIFFABLE_AMP_ELEMENTS[element.tagName]) {
    // Nodes marked with "ignore" will not be touched (old element stays).
    // We want this to allow manual diffing afterwards.
    element.setAttribute(DIFF_IGNORE, '');
  } else if (hasBinding || isAmpElement) {
    // Diff'ed node pairs with unique "key" will always be replaced.
    if (!element.hasAttribute(DIFF_KEY)) {
      element.setAttribute(DIFF_KEY, generateKey());
    }
  }
}

/**
 * @const {!Object<string, boolean>}
 * @see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md
 */
export const DENYLISTED_TAGS = {
  'applet': true,
  'audio': true,
  'base': true,
  'embed': true,
  'frame': true,
  'frameset': true,
  'iframe': true,
  'img': true,
  'link': true,
  'meta': true,
  'object': true,
  'style': true,
  'video': true,
};

/**
 * AMP elements allowed in AMP4EMAIL, modulo:
 * - amp-list and amp-state, which cannot be nested.
 * - amp-lightbox and amp-image-lightbox, which are deprecated.
 * @const {!Object<string, boolean>}
 * @see https://github.com/ampproject/amphtml/blob/master/spec/email/amp-email-components.md
 */
export const EMAIL_ALLOWLISTED_AMP_TAGS = {
  'amp-accordion': true,
  'amp-anim': true,
  'amp-bind-macro': true,
  'amp-carousel': true,
  'amp-fit-text': true,
  'amp-img': true,
  'amp-layout': true,
  'amp-selector': true,
  'amp-sidebar': true,
  'amp-timeago': true,
};

/**
 * Allowlist of tags allowed in triple mustache e.g. {{{name}}}.
 * Very restrictive by design since the triple mustache renders unescaped HTML
 * which, unlike double mustache, won't be processed by the AMP Validator.
 * @const {!Array<string>}
 */
export const TRIPLE_MUSTACHE_ALLOWLISTED_TAGS = [
  'a',
  'b',
  'br',
  'caption',
  'col',
  'colgroup',
  'code',
  'del',
  'div',
  'em',
  'hr',
  'i',
  'ins',
  'li',
  'mark',
  'ol',
  'p',
  'q',
  's',
  'small',
  'span',
  'strong',
  'sub',
  'sup',
  'table',
  'tbody',
  'time',
  'td',
  'th',
  'thead',
  'tfoot',
  'tr',
  'u',
  'ul',
];

/**
 * Tag-agnostic attribute allowlisted used by both Caja and DOMPurify.
 * @const {!Array<string>}
 */
export const ALLOWLISTED_ATTRS = [
  // AMP-only attributes that don't exist in HTML.
  'amp-fx',
  'fallback',
  'heights',
  'layout',
  'min-font-size',
  'max-font-size',
  'on',
  'option',
  'placeholder',
  // Attributes related to amp-form.
  'submitting',
  'submit-success',
  'submit-error',
  'validation-for',
  'verify-error',
  'visible-when-invalid',
  // HTML attributes that are scrubbed by Caja but we handle specially.
  'href',
  'style',
  // Attributes for amp-bind that exist in "[foo]" form.
  'text',
  // Attributes for amp-subscriptions.
  'subscriptions-action',
  'subscriptions-actions',
  'subscriptions-decorate',
  'subscriptions-dialog',
  'subscriptions-display',
  'subscriptions-section',
  'subscriptions-service',
  // Attributes for amp-nested-menu.
  'amp-nested-submenu',
  'amp-nested-submenu-open',
  'amp-nested-submenu-close',
  // A global attribute used for structured data.
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop
  'itemprop',
];

/**
 * Attributes that are only whitelisted for specific, non-AMP elements.
 * @const {!Object<string, !Array<string>>}
 */
export const ALLOWLISTED_ATTRS_BY_TAGS = {
  'a': ['rel', 'target'],
  'div': ['template'],
  'form': ['action-xhr', 'verify-xhr', 'custom-validation-reporting', 'target'],
  'input': ['mask-output'],
  'template': ['type'],
  'textarea': ['autoexpand'],
};

/** @const {!Array<string>} */
export const ALLOWLISTED_TARGETS = ['_top', '_blank'];

// Extended from IS_SCRIPT_OR_DATA in https://github.com/cure53/DOMPurify/blob/master/src/regexp.js.
const DENYLISTED_PROTOCOLS = /^(?:\w+script|data|blob):/i;

// Same as DENYLISTED_PROTOCOLS modulo those handled by DOMPurify.
const EXTENDED_DENYLISTED_PROTOCOLS = /^(?:blob):/i;

// From https://github.com/cure53/DOMPurify/blob/master/src/regexp.js.
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g;

/** @const {!Object<string, !Object<string, !RegExp>>} */
const DENYLISTED_TAG_SPECIFIC_ATTR_VALUES = Object.freeze(
  dict({
    'input': {
      'type': /(?:image|button)/i,
    },
  })
);

/**
 * Rules in addition to DENYLISTED_TAG_SPECIFIC_ATTR_VALUES for AMP4EMAIL.
 * @const {!Object<string, !Object<string, !RegExp>>}
 */
const EMAIL_DENYLISTED_TAG_SPECIFIC_ATTR_VALUES = Object.freeze(
  dict({
    'input': {
      'type': /(?:button|file|image|password)/i,
    },
  })
);

/** @const {!Array<string>} */
const DENYLISTED_FIELDS_ATTR = Object.freeze([
  'form',
  'formaction',
  'formmethod',
  'formtarget',
  'formnovalidate',
  'formenctype',
]);

/** @const {!Object<string, !Array<string>>} */
const DENYLISTED_TAG_SPECIFIC_ATTRS = Object.freeze(
  dict({
    'input': DENYLISTED_FIELDS_ATTR,
    'textarea': DENYLISTED_FIELDS_ATTR,
    'select': DENYLISTED_FIELDS_ATTR,
  })
);

/**
 * Rules in addition to denylistED_TAG_SPECIFIC_ATTRS for AMP4EMAIL.
 * @const {!Object<string, !Array<string>>}
 */
const EMAIL_DENYLISTED_TAG_SPECIFIC_ATTRS = Object.freeze(
  dict({
    'amp-anim': ['controls'],
    'form': ['name'],
  })
);

/**
 * Test for invalid `style` attribute values.
 *
 * !important avoids overriding AMP styles, while `position:fixed|sticky` is a
 * FixedLayer limitation (it only scans the style[amp-custom] stylesheet
 * for potential fixed/sticky elements). Note that the latter can be
 * circumvented with CSS comments -- not a big deal.
 *
 * @const {!RegExp}
 */
const INVALID_INLINE_STYLE_REGEX = /!important|position\s*:\s*fixed|position\s*:\s*sticky/i;

/**
 * Whether the attribute/value is valid.
 * @param {string} tagName Lowercase tag name.
 * @param {string} attrName Lowercase attribute name.
 * @param {?string} attrValue Sometimes null when called by Caja.
 * @param {!Document} doc
 * @param {boolean} opt_purify If true, skips some attribute sanitizations
 *     that are already covered by DOMPurify.
 * @return {boolean}
 */
export function isValidAttr(
  tagName,
  attrName,
  attrValue,
  doc,
  opt_purify = false
) {
  const attrValueWithoutWhitespace = attrValue
    ? attrValue.replace(ATTR_WHITESPACE, '')
    : '';

  if (!opt_purify) {
    // "on*" attributes are not allowed.
    if (startsWith(attrName, 'on') && attrName != 'on') {
      return false;
    }

    // No attributes with "<script" or "</script" in them.
    const normalized = attrValueWithoutWhitespace.toLowerCase();
    if (
      normalized.indexOf('<script') >= 0 ||
      normalized.indexOf('</script') >= 0
    ) {
      return false;
    }

    // Don't allow protocols like "javascript:".
    if (DENYLISTED_PROTOCOLS.test(attrValueWithoutWhitespace)) {
      return false;
    }
  }

  // Don't allow certain protocols that are otherwise "safe".
  // DOMPurify (opt_purify) already sanitizes javascript: etc., but
  // allows them in special cases (data URIs in images, data-* attrs).
  // So, just handle the other "extended" protocols here to avoid
  // banning "javascript:" in known-safe ARIA attributes, for example.
  if (EXTENDED_DENYLISTED_PROTOCOLS.test(attrValueWithoutWhitespace)) {
    return false;
  }

  // Don't allow certain inline style values.
  if (attrName == 'style') {
    return !INVALID_INLINE_STYLE_REGEX.test(attrValue);
  }

  // Don't allow CSS class names with internal AMP prefix.
  if (attrName == 'class' && attrValue && /(^|\W)i-amphtml-/i.test(attrValue)) {
    return false;
  }

  // Don't allow '__amp_source_origin' in URLs.
  if (isUrlAttribute(attrName) && /__amp_source_origin/.test(attrValue)) {
    return false;
  }

  const isEmail = isAmp4Email(doc);

  // Remove denylisted attributes from specific tags e.g. input[formaction].
  const attrDenylist = Object.assign(
    map(),
    DENYLISTED_TAG_SPECIFIC_ATTRS,
    isEmail ? EMAIL_DENYLISTED_TAG_SPECIFIC_ATTRS : {}
  )[tagName];
  if (attrDenylist && attrDenylist.indexOf(attrName) != -1) {
    return false;
  }

  // Remove denylisted values for specific attributes for specific tags
  // e.g. input[type=image].
  const attrValueDenylist = Object.assign(
    map(),
    DENYLISTED_TAG_SPECIFIC_ATTR_VALUES,
    isEmail ? EMAIL_DENYLISTED_TAG_SPECIFIC_ATTR_VALUES : {}
  )[tagName];
  if (attrValueDenylist) {
    const denylistedValuesRegex = attrValueDenylist[attrName];
    if (
      denylistedValuesRegex &&
      attrValue.search(denylistedValuesRegex) != -1
    ) {
      return false;
    }
  }

  return true;
}
<commit_msg>🐛 Allow dl, dt and dd on amp-mustache {{{tripleBrace}}} syntax (#28729)<commit_after>/**
 * Copyright 2019 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dict, map} from '../utils/object';
import {isAmp4Email} from '../format';
import {isUrlAttribute} from '../url-rewrite';
import {startsWith} from '../string';

/** @const {string} */
export const BIND_PREFIX = 'data-amp-bind-';

/** @const {string} */
export const DIFF_KEY = 'i-amphtml-key';

/** @const {string} */
export const DIFF_IGNORE = 'i-amphtml-ignore';

/**
 * Map of AMP element tag name to attributes that, if changed, require
 * replacement of the original element.
 * @const {!Object<string, !Array<string>>}
 */
export const DIFFABLE_AMP_ELEMENTS = {
  'AMP-IMG': ['src', 'srcset', 'layout', 'width', 'height'],
};

/**
 * Most AMP elements don't support ad hoc mutation and should be replaced
 * instead of DOM diff'ed. Some AMP elements can be manually diff'ed.
 *
 * Both of these cases require a special attribute to enable special handling in
 * the diffing algorithm. This function sets the appropriate attribute.
 *
 * @param {!Element} element
 * @param {function(): string} generateKey
 */
export function markElementForDiffing(element, generateKey) {
  const isAmpElement = startsWith(element.tagName, 'AMP-');
  // Don't DOM diff nodes with bindings because amp-bind scans newly rendered
  // elements and discards _all_ old elements _before_ diffing, so preserving
  // old elements would cause loss of functionality.
  //
  // Alternatively, we could do diffing _before_ bindings are updated and
  // more precisely add/remove bindings from nodes that set-dom diffs, but...
  // 1. Would result in content flashing unless we clone the old DOM tree.
  // 2. Need to change set-dom e.g. handle changed bind attribute values.
  const hasBinding = element.hasAttribute('i-amphtml-binding');

  if (!hasBinding && DIFFABLE_AMP_ELEMENTS[element.tagName]) {
    // Nodes marked with "ignore" will not be touched (old element stays).
    // We want this to allow manual diffing afterwards.
    element.setAttribute(DIFF_IGNORE, '');
  } else if (hasBinding || isAmpElement) {
    // Diff'ed node pairs with unique "key" will always be replaced.
    if (!element.hasAttribute(DIFF_KEY)) {
      element.setAttribute(DIFF_KEY, generateKey());
    }
  }
}

/**
 * @const {!Object<string, boolean>}
 * @see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md
 */
export const DENYLISTED_TAGS = {
  'applet': true,
  'audio': true,
  'base': true,
  'embed': true,
  'frame': true,
  'frameset': true,
  'iframe': true,
  'img': true,
  'link': true,
  'meta': true,
  'object': true,
  'style': true,
  'video': true,
};

/**
 * AMP elements allowed in AMP4EMAIL, modulo:
 * - amp-list and amp-state, which cannot be nested.
 * - amp-lightbox and amp-image-lightbox, which are deprecated.
 * @const {!Object<string, boolean>}
 * @see https://github.com/ampproject/amphtml/blob/master/spec/email/amp-email-components.md
 */
export const EMAIL_ALLOWLISTED_AMP_TAGS = {
  'amp-accordion': true,
  'amp-anim': true,
  'amp-bind-macro': true,
  'amp-carousel': true,
  'amp-fit-text': true,
  'amp-img': true,
  'amp-layout': true,
  'amp-selector': true,
  'amp-sidebar': true,
  'amp-timeago': true,
};

/**
 * Allowlist of tags allowed in triple mustache e.g. {{{name}}}.
 * Very restrictive by design since the triple mustache renders unescaped HTML
 * which, unlike double mustache, won't be processed by the AMP Validator.
 * @const {!Array<string>}
 */
export const TRIPLE_MUSTACHE_ALLOWLISTED_TAGS = [
  'a',
  'b',
  'br',
  'caption',
  'code',
  'col',
  'colgroup',
  'dd',
  'del',
  'div',
  'dl',
  'dt',
  'em',
  'hr',
  'i',
  'ins',
  'li',
  'mark',
  'ol',
  'p',
  'q',
  's',
  'small',
  'span',
  'strong',
  'sub',
  'sup',
  'table',
  'tbody',
  'td',
  'tfoot',
  'th',
  'thead',
  'time',
  'tr',
  'u',
  'ul',
];

/**
 * Tag-agnostic attribute allowlisted used by both Caja and DOMPurify.
 * @const {!Array<string>}
 */
export const ALLOWLISTED_ATTRS = [
  // AMP-only attributes that don't exist in HTML.
  'amp-fx',
  'fallback',
  'heights',
  'layout',
  'min-font-size',
  'max-font-size',
  'on',
  'option',
  'placeholder',
  // Attributes related to amp-form.
  'submitting',
  'submit-success',
  'submit-error',
  'validation-for',
  'verify-error',
  'visible-when-invalid',
  // HTML attributes that are scrubbed by Caja but we handle specially.
  'href',
  'style',
  // Attributes for amp-bind that exist in "[foo]" form.
  'text',
  // Attributes for amp-subscriptions.
  'subscriptions-action',
  'subscriptions-actions',
  'subscriptions-decorate',
  'subscriptions-dialog',
  'subscriptions-display',
  'subscriptions-section',
  'subscriptions-service',
  // Attributes for amp-nested-menu.
  'amp-nested-submenu',
  'amp-nested-submenu-open',
  'amp-nested-submenu-close',
  // A global attribute used for structured data.
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop
  'itemprop',
];

/**
 * Attributes that are only whitelisted for specific, non-AMP elements.
 * @const {!Object<string, !Array<string>>}
 */
export const ALLOWLISTED_ATTRS_BY_TAGS = {
  'a': ['rel', 'target'],
  'div': ['template'],
  'form': ['action-xhr', 'verify-xhr', 'custom-validation-reporting', 'target'],
  'input': ['mask-output'],
  'template': ['type'],
  'textarea': ['autoexpand'],
};

/** @const {!Array<string>} */
export const ALLOWLISTED_TARGETS = ['_top', '_blank'];

// Extended from IS_SCRIPT_OR_DATA in https://github.com/cure53/DOMPurify/blob/master/src/regexp.js.
const DENYLISTED_PROTOCOLS = /^(?:\w+script|data|blob):/i;

// Same as DENYLISTED_PROTOCOLS modulo those handled by DOMPurify.
const EXTENDED_DENYLISTED_PROTOCOLS = /^(?:blob):/i;

// From https://github.com/cure53/DOMPurify/blob/master/src/regexp.js.
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g;

/** @const {!Object<string, !Object<string, !RegExp>>} */
const DENYLISTED_TAG_SPECIFIC_ATTR_VALUES = Object.freeze(
  dict({
    'input': {
      'type': /(?:image|button)/i,
    },
  })
);

/**
 * Rules in addition to DENYLISTED_TAG_SPECIFIC_ATTR_VALUES for AMP4EMAIL.
 * @const {!Object<string, !Object<string, !RegExp>>}
 */
const EMAIL_DENYLISTED_TAG_SPECIFIC_ATTR_VALUES = Object.freeze(
  dict({
    'input': {
      'type': /(?:button|file|image|password)/i,
    },
  })
);

/** @const {!Array<string>} */
const DENYLISTED_FIELDS_ATTR = Object.freeze([
  'form',
  'formaction',
  'formmethod',
  'formtarget',
  'formnovalidate',
  'formenctype',
]);

/** @const {!Object<string, !Array<string>>} */
const DENYLISTED_TAG_SPECIFIC_ATTRS = Object.freeze(
  dict({
    'input': DENYLISTED_FIELDS_ATTR,
    'textarea': DENYLISTED_FIELDS_ATTR,
    'select': DENYLISTED_FIELDS_ATTR,
  })
);

/**
 * Rules in addition to denylistED_TAG_SPECIFIC_ATTRS for AMP4EMAIL.
 * @const {!Object<string, !Array<string>>}
 */
const EMAIL_DENYLISTED_TAG_SPECIFIC_ATTRS = Object.freeze(
  dict({
    'amp-anim': ['controls'],
    'form': ['name'],
  })
);

/**
 * Test for invalid `style` attribute values.
 *
 * !important avoids overriding AMP styles, while `position:fixed|sticky` is a
 * FixedLayer limitation (it only scans the style[amp-custom] stylesheet
 * for potential fixed/sticky elements). Note that the latter can be
 * circumvented with CSS comments -- not a big deal.
 *
 * @const {!RegExp}
 */
const INVALID_INLINE_STYLE_REGEX = /!important|position\s*:\s*fixed|position\s*:\s*sticky/i;

/**
 * Whether the attribute/value is valid.
 * @param {string} tagName Lowercase tag name.
 * @param {string} attrName Lowercase attribute name.
 * @param {?string} attrValue Sometimes null when called by Caja.
 * @param {!Document} doc
 * @param {boolean} opt_purify If true, skips some attribute sanitizations
 *     that are already covered by DOMPurify.
 * @return {boolean}
 */
export function isValidAttr(
  tagName,
  attrName,
  attrValue,
  doc,
  opt_purify = false
) {
  const attrValueWithoutWhitespace = attrValue
    ? attrValue.replace(ATTR_WHITESPACE, '')
    : '';

  if (!opt_purify) {
    // "on*" attributes are not allowed.
    if (startsWith(attrName, 'on') && attrName != 'on') {
      return false;
    }

    // No attributes with "<script" or "</script" in them.
    const normalized = attrValueWithoutWhitespace.toLowerCase();
    if (
      normalized.indexOf('<script') >= 0 ||
      normalized.indexOf('</script') >= 0
    ) {
      return false;
    }

    // Don't allow protocols like "javascript:".
    if (DENYLISTED_PROTOCOLS.test(attrValueWithoutWhitespace)) {
      return false;
    }
  }

  // Don't allow certain protocols that are otherwise "safe".
  // DOMPurify (opt_purify) already sanitizes javascript: etc., but
  // allows them in special cases (data URIs in images, data-* attrs).
  // So, just handle the other "extended" protocols here to avoid
  // banning "javascript:" in known-safe ARIA attributes, for example.
  if (EXTENDED_DENYLISTED_PROTOCOLS.test(attrValueWithoutWhitespace)) {
    return false;
  }

  // Don't allow certain inline style values.
  if (attrName == 'style') {
    return !INVALID_INLINE_STYLE_REGEX.test(attrValue);
  }

  // Don't allow CSS class names with internal AMP prefix.
  if (attrName == 'class' && attrValue && /(^|\W)i-amphtml-/i.test(attrValue)) {
    return false;
  }

  // Don't allow '__amp_source_origin' in URLs.
  if (isUrlAttribute(attrName) && /__amp_source_origin/.test(attrValue)) {
    return false;
  }

  const isEmail = isAmp4Email(doc);

  // Remove denylisted attributes from specific tags e.g. input[formaction].
  const attrDenylist = Object.assign(
    map(),
    DENYLISTED_TAG_SPECIFIC_ATTRS,
    isEmail ? EMAIL_DENYLISTED_TAG_SPECIFIC_ATTRS : {}
  )[tagName];
  if (attrDenylist && attrDenylist.indexOf(attrName) != -1) {
    return false;
  }

  // Remove denylisted values for specific attributes for specific tags
  // e.g. input[type=image].
  const attrValueDenylist = Object.assign(
    map(),
    DENYLISTED_TAG_SPECIFIC_ATTR_VALUES,
    isEmail ? EMAIL_DENYLISTED_TAG_SPECIFIC_ATTR_VALUES : {}
  )[tagName];
  if (attrValueDenylist) {
    const denylistedValuesRegex = attrValueDenylist[attrName];
    if (
      denylistedValuesRegex &&
      attrValue.search(denylistedValuesRegex) != -1
    ) {
      return false;
    }
  }

  return true;
}
<|endoftext|>