From 37f19a02569833a9e49d2473439d9739444e78f8 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Wed, 25 Jan 2017 14:00:13 +0600 Subject: [PATCH 01/80] Fixes #13300, #13307, #13310, #13312 - Bug #13300: Allow pjax with "data-pjax" with no value in `yii.js`. - Bug #13307: Preventing of race conditions in script filter in `yii.js` works incorrectly. - Bug #13310: Handle relative and absolute URLs coincidence in CSS filter in `yii.js`. - Bug #13312: `skipOuterContainers` option was incorrectly passed to pjax in `handleAction` in `yii.js`. - Partially fixes #13299. Adds tests for #8014, #11921, #10974, #11494, #10358, #10097. --- framework/CHANGELOG.md | 4 + framework/assets/yii.js | 404 +++++++------ framework/web/Response.php | 2 +- tests/js/data/yii.html | 194 ++++++ tests/js/tests/yii.test.js | 1425 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1848 insertions(+), 181 deletions(-) create mode 100644 tests/js/data/yii.html create mode 100644 tests/js/tests/yii.test.js diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9032aeb..fee219d 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -46,6 +46,10 @@ Yii Framework 2 Change Log - Bug #13231: Fixed `destroy` method in `yii.gridView.js` which did not work as expected (arogachev) - Bug #13232: Event handlers were not detached with changed selector in `yii.gridView.js` (arogachev) - Bug #13277: Fixed invalid parsing of `--` ("End of Options" special argument) in CLI (rugabarbo) +- Bug #13300: Allow pjax with "data-pjax" with no value in `yii.js` (arogachev) +- Bug #13307: Preventing of race conditions in script filter in `yii.js` works incorrectly (arogachev) +- Bug #13310: Handle relative and absolute URLs coincidence in CSS filter in `yii.js` (arogachev) +- Bug #13312: `skipOuterContainers` option was incorrectly passed to pjax in `handleAction` in `yii.js` (arogachev) - Bug #13309: Fixes incorrect console width/height detecting with using Stty on Mac (nowm) - Bug #13326: Fixed wrong background color generation in `BaseConsole::renderColoredString()` (nowm, silverfire) - Bug #12133: Fixed `getDbTargets()` function in `yii\log\migrations\m141106_185632_log_init` that would create a log table correctly (bumstik) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index 73484ef..b37451b 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -17,13 +17,13 @@ * A module may be structured as follows: * * ```javascript - * yii.sample = (function($) { + * window.yii.sample = (function($) { * var pub = { * // whether this module is currently active. If false, init() will not be called for this module * // it will also not be called for all its child modules. If this property is undefined, it means true. * isActive: true, * init: function() { - * // ... module initialization code go here ... + * // ... module initialization code goes here ... * }, * * // ... other public functions and properties go here ... @@ -32,7 +32,7 @@ * // ... private functions and properties go here ... * * return pub; - * })(jQuery); + * })(window.jQuery); * ``` * * Using this structure, you can define public and private functions/properties for a module. @@ -46,9 +46,9 @@ window.yii = (function ($) { /** * List of JS or CSS URLs that can be loaded multiple times via AJAX requests. * Each item may be represented as either an absolute URL or a relative one. - * Each item may contain a wildcart matching character `*`, that means one or more + * Each item may contain a wildcard matching character `*`, that means one or more * any characters on the position. For example: - * - `/css/*.js` will match any file ending with `.js` in the `css` directory of the current web site + * - `/css/*.css` will match any file ending with `.css` in the `css` directory of the current web site * - `http*://cdn.example.com/*` will match any files on domain `cdn.example.com`, loaded with HTTP or HTTPS * - `/js/myCustomScript.js?realm=*` will match file `/js/myCustomScript.js` with defined `realm` parameter */ @@ -56,7 +56,8 @@ window.yii = (function ($) { /** * The selector for clickable elements that need to support confirmation and form submission. */ - clickableSelector: 'a, button, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"]', + clickableSelector: 'a, button, input[type="submit"], input[type="button"], input[type="reset"], ' + + 'input[type="image"]', /** * The selector for changeable elements that need to support confirmation and form submission. */ @@ -107,7 +108,7 @@ window.yii = (function ($) { * @param cancel a callback to be called when the user cancels the confirmation */ confirm: function (message, ok, cancel) { - if (confirm(message)) { + if (window.confirm(message)) { !ok || ok(); } else { !cancel || cancel(); @@ -143,74 +144,70 @@ window.yii = (function ($) { * 'name2' => 'value2', * ], * ], - * ]; + * ]); * ``` * * @param $e the jQuery representation of the element + * @param event Related event */ handleAction: function ($e, event) { var $form = $e.attr('data-form') ? $('#' + $e.attr('data-form')) : $e.closest('form'), method = !$e.data('method') && $form ? $form.attr('method') : $e.data('method'), action = $e.attr('href'), + isValidAction = action && action !== '#', params = $e.data('params'), - pjax = $e.data('pjax') || 0, - usePjax = pjax !== 0 && $.support.pjax, - pjaxPushState = !!$e.data('pjax-push-state'), - pjaxReplaceState = !!$e.data('pjax-replace-state'), - pjaxTimeout = $e.data('pjax-timeout'), - pjaxScrollTo = $e.data('pjax-scrollto'), - pjaxPushRedirect = $e.data('pjax-push-redirect'), - pjaxReplaceRedirect = $e.data('pjax-replace-redirect'), - pjaxSkipOuterContainers = $e.data('pjax-skip-outer-containers'), + areValidParams = params && $.isPlainObject(params), + pjax = $e.data('pjax'), + usePjax = pjax !== undefined && pjax !== 0 && $.support.pjax, pjaxContainer, pjaxOptions = {}; if (usePjax) { - if ($e.data('pjax-container')) { - pjaxContainer = $e.data('pjax-container'); - } else { - pjaxContainer = $e.closest('[data-pjax-container=""]'); - } - // default to body if pjax container not found + pjaxContainer = $e.data('pjax-container') || $e.closest('[data-pjax-container]'); if (!pjaxContainer.length) { pjaxContainer = $('body'); } pjaxOptions = { container: pjaxContainer, - push: pjaxPushState, - replace: pjaxReplaceState, - scrollTo: pjaxScrollTo, - pushRedirect: pjaxPushRedirect, - replaceRedirect: pjaxReplaceRedirect, - pjaxSkipOuterContainers: pjaxSkipOuterContainers, - timeout: pjaxTimeout, + push: !!$e.data('pjax-push-state'), + replace: !!$e.data('pjax-replace-state'), + scrollTo: $e.data('pjax-scrollto'), + pushRedirect: $e.data('pjax-push-redirect'), + replaceRedirect: $e.data('pjax-replace-redirect'), + skipOuterContainers: $e.data('pjax-skip-outer-containers'), + timeout: $e.data('pjax-timeout'), originalEvent: event, originalTarget: $e - } + }; } if (method === undefined) { - if (action && action != '#') { - if (usePjax) { - $.pjax.click(event, pjaxOptions); - } else { - window.location = action; - } + if (isValidAction) { + usePjax ? $.pjax.click(event, pjaxOptions) : window.location.assign(action); } else if ($e.is(':submit') && $form.length) { if (usePjax) { - $form.on('submit',function(e){ + $form.on('submit', function (e) { $.pjax.submit(e, pjaxOptions); - }) + }); } $form.trigger('submit'); } return; } - var newForm = !$form.length; - if (newForm) { - if (!action || !/(^\/|:\/\/)/.test(action)) { - action = window.location.href; + var oldMethod, + oldAction, + newForm = !$form.length; + if (!newForm) { + oldMethod = $form.attr('method'); + $form.attr('method', method); + if (isValidAction) { + oldAction = $form.attr('action'); + $form.attr('action', action); + } + } else { + if (!isValidAction) { + action = pub.getCurrentUrl(); } $form = $('
', {method: method, action: action}); var target = $e.attr('target'); @@ -219,9 +216,10 @@ window.yii = (function ($) { } if (!/(get|post)/i.test(method)) { $form.append($('', {name: '_method', value: method, type: 'hidden'})); - method = 'POST'; + method = 'post'; + $form.attr('method', method); } - if (!/(get|head|options)/i.test(method)) { + if (/post/i.test(method)) { var csrfParam = pub.getCsrfParam(); if (csrfParam) { $form.append($('', {name: csrfParam, value: pub.getCsrfToken(), type: 'hidden'})); @@ -232,49 +230,41 @@ window.yii = (function ($) { var activeFormData = $form.data('yiiActiveForm'); if (activeFormData) { - // remember who triggers the form submission. This is used by yii.activeForm.js + // Remember the element triggered the form submission. This is used by yii.activeForm.js. activeFormData.submitObject = $e; } - // temporarily add hidden inputs according to data-params - if (params && $.isPlainObject(params)) { - $.each(params, function (idx, obj) { - $form.append($('').attr({name: idx, value: obj, type: 'hidden'})); + if (areValidParams) { + $.each(params, function (name, value) { + $form.append($('').attr({name: name, value: value, type: 'hidden'})); }); } - var oldMethod = $form.attr('method'); - $form.attr('method', method); - var oldAction = null; - if (action && action != '#') { - oldAction = $form.attr('action'); - $form.attr('action', action); - } if (usePjax) { - $form.on('submit',function(e){ + $form.on('submit', function (e) { $.pjax.submit(e, pjaxOptions); - }) + }); } + $form.trigger('submit'); - $.when($form.data('yiiSubmitFinalizePromise')).then( - function () { - if (oldAction != null) { - $form.attr('action', oldAction); - } - $form.attr('method', oldMethod); - // remove the temporarily added hidden inputs - if (params && $.isPlainObject(params)) { - $.each(params, function (idx, obj) { - $('input[name="' + idx + '"]', $form).remove(); - }); - } + $.when($form.data('yiiSubmitFinalizePromise')).then(function () { + if (newForm) { + $form.remove(); + return; + } - if (newForm) { - $form.remove(); - } + if (oldAction !== undefined) { + $form.attr('action', oldAction); + } + $form.attr('method', oldMethod); + + if (areValidParams) { + $.each(params, function (name) { + $('input[name="' + name + '"]', $form).remove(); + }); } - ); + }); }, getQueryParams: function (url) { @@ -284,67 +274,183 @@ window.yii = (function ($) { } var pairs = url.substring(pos + 1).split('#')[0].split('&'), - params = {}, - pair, - i; + params = {}; - for (i = 0; i < pairs.length; i++) { - pair = pairs[i].split('='); + for (var i = 0, len = pairs.length; i < len; i++) { + var pair = pairs[i].split('='); var name = decodeURIComponent(pair[0].replace(/\+/g, '%20')); var value = decodeURIComponent(pair[1].replace(/\+/g, '%20')); - if (name.length) { - if (params[name] !== undefined) { - if (!$.isArray(params[name])) { - params[name] = [params[name]]; - } - params[name].push(value || ''); - } else { - params[name] = value || ''; + if (!name.length) { + continue; + } + if (params[name] === undefined) { + params[name] = value || ''; + } else { + if (!$.isArray(params[name])) { + params[name] = [params[name]]; } + params[name].push(value || ''); } } return params; }, initModule: function (module) { - if (module.isActive === undefined || module.isActive) { - if ($.isFunction(module.init)) { - module.init(); - } - $.each(module, function () { - if ($.isPlainObject(this)) { - pub.initModule(this); - } - }); + if (module.isActive !== undefined && !module.isActive) { + return; } + if ($.isFunction(module.init)) { + module.init(); + } + $.each(module, function () { + if ($.isPlainObject(this)) { + pub.initModule(this); + } + }); }, init: function () { initCsrfHandler(); initRedirectHandler(); - initScriptFilter(); + initAssetFilters(); initDataMethods(); + }, + + /** + * Returns the URL of the current page without params and trailing slash. Separated and made public for testing. + * @returns {string} + */ + getBaseCurrentUrl: function () { + return window.location.protocol + '//' + window.location.host; + }, + + /** + * Returns the URL of the current page. Used for testing, you can always call `window.location.href` manually + * instead. + * @returns {string} + */ + getCurrentUrl: function () { + return window.location.href; } }; + function initCsrfHandler() { + // automatically send CSRF token for all AJAX requests + $.ajaxPrefilter(function (options, originalOptions, xhr) { + if (!options.crossDomain && pub.getCsrfParam()) { + xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); + } + }); + pub.refreshCsrfToken(); + } + function initRedirectHandler() { // handle AJAX redirection - $(document).ajaxComplete(function (event, xhr, settings) { + $(document).ajaxComplete(function (event, xhr) { var url = xhr && xhr.getResponseHeader('X-Redirect'); if (url) { - window.location = url; + window.location.assign(url); } }); } - function initCsrfHandler() { - // automatically send CSRF token for all AJAX requests - $.ajaxPrefilter(function (options, originalOptions, xhr) { - if (!options.crossDomain && pub.getCsrfParam()) { - xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); + function initAssetFilters() { + /** + * Used for storing loaded scripts and information about loading each script if it's in the process of loading. + * A single script can have one of the following values: + * + * - `undefined` - script was not loaded at all before or was loaded with error last time. + * - `true` (boolean) - script was successfully loaded. + * - object - script is currently loading. + * + * In case of a value being an object the properties are: + * - `xhrList` - represents a queue of XHR requests sent to the same URL (related with this script) in the same + * small period of time. + * - `xhrDone` - boolean, acts like a locking mechanism. When one of the XHR requests in the queue is + * successfully completed, it will abort the rest of concurrent requests to the same URL until cleanup is done + * to prevent possible errors and race conditions. + * @type {{}} + */ + var loadedScripts = {}; + + $('script[src]').each(function () { + var url = getAbsoluteUrl(this.src); + loadedScripts[url] = true; + }); + + $.ajaxPrefilter('script', function (options, originalOptions, xhr) { + if (options.dataType == 'jsonp') { + return; } + + var url = getAbsoluteUrl(options.url), + forbiddenRepeatedLoad = loadedScripts[url] === true && !isReloadableAsset(url), + cleanupRunning = loadedScripts[url] !== undefined && loadedScripts[url]['xhrDone'] === true; + + if (forbiddenRepeatedLoad || cleanupRunning) { + xhr.abort(); + return; + } + + if (loadedScripts[url] === undefined || loadedScripts[url] === true) { + loadedScripts[url] = { + xhrList: [], + xhrDone: false + }; + } + + xhr.done(function (data, textStatus, jqXHR) { + // If multiple requests were successfully loaded, perform cleanup only once + if (loadedScripts[jqXHR.yiiUrl]['xhrDone'] === true) { + return; + } + + loadedScripts[jqXHR.yiiUrl]['xhrDone'] = true; + + for (var i = 0, len = loadedScripts[jqXHR.yiiUrl]['xhrList'].length; i < len; i++) { + var singleXhr = loadedScripts[jqXHR.yiiUrl]['xhrList'][i]; + if (singleXhr && singleXhr.readyState !== XMLHttpRequest.DONE) { + singleXhr.abort(); + } + } + + loadedScripts[jqXHR.yiiUrl] = true; + }).fail(function (jqXHR, textStatus) { + if (textStatus === 'abort') { + return; + } + + delete loadedScripts[jqXHR.yiiUrl]['xhrList'][jqXHR.yiiIndex]; + + var allFailed = true; + for (var i = 0, len = loadedScripts[jqXHR.yiiUrl]['xhrList'].length; i < len; i++) { + if (loadedScripts[jqXHR.yiiUrl]['xhrList'][i]) { + allFailed = false; + } + } + + if (allFailed) { + delete loadedScripts[jqXHR.yiiUrl]; + } + }); + // Use prefix for custom XHR properties to avoid possible conflicts with existing properties + xhr.yiiIndex = loadedScripts[url]['xhrList'].length; + xhr.yiiUrl = url; + + loadedScripts[url]['xhrList'][xhr.yiiIndex] = xhr; + }); + + $(document).ajaxComplete(function () { + var styleSheets = []; + $('link[rel=stylesheet]').each(function () { + var url = getAbsoluteUrl(this.href); + if (isReloadableAsset(url)) { + return; + } + + $.inArray(url, styleSheets) === -1 ? styleSheets.push(url) : $(this).remove(); + }); }); - pub.refreshCsrfToken(); } function initDataMethods() { @@ -374,13 +480,9 @@ window.yii = (function ($) { .on('change.yii', pub.changeableSelector, handler); } - function isReloadable(url) { - var hostInfo = getHostInfo(); - + function isReloadableAsset(url) { for (var i = 0; i < pub.reloadableScripts.length; i++) { - var rule = pub.reloadableScripts[i]; - rule = rule.charAt(0) === '/' ? hostInfo + rule : rule; - + var rule = getAbsoluteUrl(pub.reloadableScripts[i]); var match = new RegExp("^" + escapeRegExp(rule).split('\\*').join('.*') + "$").test(url); if (match === true) { return true; @@ -395,76 +497,18 @@ window.yii = (function ($) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } - function getHostInfo() { - return location.protocol + '//' + location.host; - } - - function initScriptFilter() { - var hostInfo = getHostInfo(); - var loadedScripts = {}; - - var scripts = $('script[src]').map(function () { - return this.src.charAt(0) === '/' ? hostInfo + this.src : this.src; - }).toArray(); - for (var i = 0, len = scripts.length; i < len; i++) { - loadedScripts[scripts[i]] = true; - } - - $.ajaxPrefilter('script', function (options, originalOptions, xhr) { - if (options.dataType == 'jsonp') { - return; - } - - var url = options.url.charAt(0) === '/' ? hostInfo + options.url : options.url; - - if (url in loadedScripts) { - var item = loadedScripts[url]; - - // If the concurrent XHR request is running and URL is not reloadable - if (item !== true && !isReloadable(url)) { - // Abort the current XHR request when previous finished successfully - item.done(function () { - if (xhr && xhr.readyState !== 4) { - xhr.abort(); - } - }); - // Or abort previous XHR if the current one is loaded faster - xhr.done(function () { - if (item && item.readyState !== 4) { - item.abort(); - } - }); - } else if (!isReloadable(url)) { - xhr.abort(); - } - } else { - loadedScripts[url] = xhr.done(function () { - loadedScripts[url] = true; - }).fail(function () { - delete loadedScripts[url]; - }); - } - }); - - $(document).ajaxComplete(function (event, xhr, settings) { - var styleSheets = []; - $('link[rel=stylesheet]').each(function () { - if (isReloadable(this.href)) { - return; - } - if ($.inArray(this.href, styleSheets) == -1) { - styleSheets.push(this.href) - } else { - $(this).remove(); - } - }) - }); + /** + * Returns absolute URL based on the given URL + * @param {string} url Initial URL + * @returns {string} + */ + function getAbsoluteUrl(url) { + return url.charAt(0) === '/' ? pub.getBaseCurrentUrl() + url : url; } return pub; -})(jQuery); +})(window.jQuery); -jQuery(function () { - yii.initModule(yii); +window.jQuery(function () { + window.yii.initModule(window.yii); }); - diff --git a/framework/web/Response.php b/framework/web/Response.php index 2ec4d6b..539966e 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -782,7 +782,7 @@ class Response extends \yii\base\Response * * ```javascript * $document.ajaxComplete(function (event, xhr, settings) { - * var url = xhr.getResponseHeader('X-Redirect'); + * var url = xhr && xhr.getResponseHeader('X-Redirect'); * if (url) { * window.location = url; * } diff --git a/tests/js/data/yii.html b/tests/js/data/yii.html new file mode 100644 index 0000000..ce040d6 --- /dev/null +++ b/tests/js/data/yii.html @@ -0,0 +1,194 @@ + + + + + + + + +
+ + + + +
+ +
+
+ +
+ + +
+
+
+ + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+
+
+ +
+ + + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + + +
+ +
+
+
+ +
+
+ + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + +
+ + + +
+ + +
+
+
+ +
+ + +
+ +
+
+ +
+
+ + + +
+ +
+ + + +
+
+ + diff --git a/tests/js/tests/yii.test.js b/tests/js/tests/yii.test.js new file mode 100644 index 0000000..ce0ce7c --- /dev/null +++ b/tests/js/tests/yii.test.js @@ -0,0 +1,1425 @@ +var assert = require('chai').assert; +var sinon; +var withData = require('leche').withData; +var jsdom = require('mocha-jsdom'); + +var fs = require('fs'); +var vm = require('vm'); + +var StringUtils = { + /** + * Removes line breaks and redundant whitespaces from the given string. Used to compare HTML strings easier, + * regardless of the formatting. + * @param str Initial string to clean + * @returns {string} Cleaned string + */ + cleanHTML: function (str) { + return str.replace(/\r?\n|\r|\s\s+/g, ''); + } +}; + +describe('yii', function () { + var yiiPath = 'framework/assets/yii.js'; + var jQueryPath = 'vendor/bower/jquery/dist/jquery.js'; + var pjaxPath = 'vendor/bower/yii2-pjax/jquery.pjax.js'; + var sandbox; + var $; + var yii; + var yiiGetBaseCurrentUrlStub; + var yiiGetCurrentUrlStub; + + function registerPjax() { + var code = fs.readFileSync(pjaxPath); + var script = new vm.Script(code); + var sandbox = {jQuery: $, window: window, navigator: window.navigator}; + var context = new vm.createContext(sandbox); + script.runInContext(context); + } + + function registerTestableCode() { + registerPjax(); + + var code = fs.readFileSync(yiiPath); + var script = new vm.Script(code); + sandbox = {window: window, document: window.document, XMLHttpRequest: window.XMLHttpRequest}; + var context = new vm.createContext(sandbox); + + script.runInContext(context); + yii = sandbox.window.yii; + } + + /** + * Mapping of pjax data attributes with according plugin options + * @type {{}} + */ + var pjaxAttributes = { + 'data-pjax-push-state': 'push', + 'data-pjax-replace-state': 'replace', + 'data-pjax-scrollto': 'scrollTo', + 'data-pjax-push-redirect': 'pushRedirect', + 'data-pjax-replace-redirect': 'replaceRedirect', + 'data-pjax-skip-outer-containers': 'skipOuterContainers', + 'data-pjax-timeout': 'timeout' + }; + + /** + * Add pjax related attributes to all elements with "data-pjax" attribute. Used to prevent copy pasting and for + * better readability of the test HTML data. + */ + function addPjaxAttributes() { + $.each(pjaxAttributes, function (name, value) { + $('[data-pjax]').attr(name, value); + }); + } + + jsdom({ + html: fs.readFileSync('tests/js/data/yii.html', 'utf-8'), + src: fs.readFileSync(jQueryPath, 'utf-8') + }); + + before(function () { + $ = window.$; + registerTestableCode(); + sinon = require('sinon'); + addPjaxAttributes(); + yiiGetBaseCurrentUrlStub = sinon.stub(yii, 'getBaseCurrentUrl', function () { + return 'http://foo.bar'; + }); + yiiGetCurrentUrlStub = sinon.stub(yii, 'getCurrentUrl', function () { + return 'http://foo.bar/'; + }); + }); + + after(function () { + yiiGetBaseCurrentUrlStub.restore(); + yiiGetCurrentUrlStub.restore(); + }); + + describe('getCsrfParam method', function () { + it('should return current CSRF parameter name', function () { + assert.equal(yii.getCsrfParam(), '_csrf'); + }); + }); + + describe('getCsrfToken method', function () { + it('should return current CSRF parameter value', function () { + assert.equal(yii.getCsrfToken(), 'foobar'); + }); + }); + + describe('CSRF modifying methods', function () { + var initialCsrfParam; + var initialCsrfToken; + + beforeEach(function () { + initialCsrfParam = $('meta[name="csrf-param"]').attr('content'); + initialCsrfToken = $('meta[name="csrf-token"]').attr('content'); + }); + + // Restore CSRF parameter name and value to initial values because they are used in different tests + + afterEach(function () { + $('meta[name="csrf-param"]').attr('content', initialCsrfParam); + $('meta[name="csrf-token"]').attr('content', initialCsrfToken); + }); + + describe('setCsrfToken method', function () { + it('should update CSRF parameter name and value with new values', function () { + yii.setCsrfToken('_csrf1', 'foobar1'); + + assert.equal(yii.getCsrfParam(), '_csrf1'); + assert.equal(yii.getCsrfToken(), 'foobar1'); + }); + }); + + describe('refreshCsrfToken method', function () { + it('should assign CSRF token values for all forms during initialization', function () { + assert.equal($('#form1').find('input[name="_csrf"]').val(), 'foobar'); + assert.equal($('#form2').find('input[name="_csrf"]').val(), 'foobar'); + }); + + it('should update CSRF token values for all forms after modifying current CSRF token value', function () { + $('meta[name="csrf-token"]').attr('content', 'foobar1'); + yii.refreshCsrfToken(); + + assert.equal($('#form1').find('input[name="_csrf"]').val(), 'foobar1'); + assert.equal($('#form2').find('input[name="_csrf"]').val(), 'foobar1'); + }); + }); + }); + + describe('confirm method', function () { + var windowConfirmStub; + var confirmed; + var okSpy; + var cancelSpy; + + beforeEach(function () { + windowConfirmStub = sinon.stub(window, 'confirm', function () { + return confirmed; + }); + okSpy = sinon.spy(); + cancelSpy = sinon.spy(); + }); + + afterEach(function () { + windowConfirmStub.restore(); + okSpy.reset(); + cancelSpy.reset(); + }); + + withData({ + 'ok and cancel not set, "OK" selected': [{ + setOk: false, + setCancel: false, + confirmChoice: true, + expectOkCalled: false, + expectCancelCalled: false + }], + 'ok and cancel not set, "Cancel" selected': [{ + setOk: false, + setCancel: false, + confirmChoice: false, + expectOkCalled: false, + expectCancelCalled: false + }], + 'ok set, "OK" selected': [{ + setOk: true, + setCancel: false, + confirmChoice: true, + expectOkCalled: true, + expectCancelCalled: false + }], + 'ok set, "Cancel" selected': [{ + setOk: true, + setCancel: false, + confirmChoice: false, + expectOkCalled: false, + expectCancelCalled: false + }], + 'cancel set, "OK" selected': [{ + setOk: false, + setCancel: true, + confirmChoice: true, + expectOkCalled: false, + expectCancelCalled: false + }], + 'cancel set, "Cancel" selected': [{ + setOk: false, + setCancel: true, + confirmChoice: false, + expectOkCalled: false, + expectCancelCalled: true + }], + 'ok and cancel set, "OK" selected': [{ + setOk: true, + setCancel: true, + confirmChoice: true, + expectOkCalled: true, + expectCancelCalled: false + }], + 'ok and cancel set, "Cancel" selected': [{ + setOk: true, + setCancel: true, + confirmChoice: false, + expectOkCalled: false, + expectCancelCalled: true + }] + }, function (data) { + var setOk = data.setOk; + var setCancel = data.setCancel; + var confirmChoice = data.confirmChoice; + var expectOkCalled = data.expectOkCalled; + var expectCancelCalled = data.expectCancelCalled; + + var message = 'should return undefined, confirm should be called once with according message, '; + if (expectOkCalled && !expectCancelCalled) { + message += 'ok callback should be called once'; + } else if (!expectOkCalled && expectCancelCalled) { + message += 'cancel callback should be called once'; + } else if (!expectOkCalled && !expectCancelCalled) { + message += 'ok and cancel callbacks should not be called'; + } else { + message += 'ok and cancel callbacks should be called once'; + } + + it(message, function () { + confirmed = confirmChoice; + + var result = yii.confirm('Are you sure?', setOk ? okSpy : undefined, setCancel ? cancelSpy : undefined); + + assert.isUndefined(result); + assert.isTrue(windowConfirmStub.calledOnce); + assert.deepEqual(windowConfirmStub.getCall(0).args, ['Are you sure?']); + expectOkCalled ? assert.isTrue(okSpy.calledOnce) : assert.isFalse(okSpy.called); + expectCancelCalled ? assert.isTrue(cancelSpy.calledOnce) : assert.isFalse(cancelSpy.called); + }); + }); + }); + + describe('handleAction method', function () { + var windowLocationAssignStub; + var pjaxClickStub; + var pjaxSubmitStub; + var formSubmitsCount; + var initialFormsCount; + var $savedSubmittedForm; + + beforeEach(function () { + windowLocationAssignStub = sinon.stub(window.location, 'assign'); + pjaxClickStub = sinon.stub($.pjax, 'click'); + pjaxSubmitStub = sinon.stub($.pjax, 'submit'); + initialFormsCount = $('form').length; + countFormSubmits(); + }); + + afterEach(function () { + windowLocationAssignStub.restore(); + pjaxClickStub.restore(); + pjaxSubmitStub.restore(); + formSubmitsCount = undefined; + initialFormsCount = undefined; + $savedSubmittedForm = undefined; + $(document).off('submit'); + $('form').off('submit'); + }); + + function countFormSubmits() { + formSubmitsCount = 0; + $(document).on('submit', 'form', function () { + formSubmitsCount++; + $savedSubmittedForm = $(this).clone(); + + return false; + }); + } + + function verifyNoActions() { + assert.isFalse(windowLocationAssignStub.called); + assert.isFalse(pjaxClickStub.called); + + assert.equal(formSubmitsCount, 0); + assert.isFalse(pjaxSubmitStub.called); + assert.equal($('form').length, initialFormsCount); + } + + function verifyPageLoad(url) { + assert.isTrue(windowLocationAssignStub.calledOnce); + assert.deepEqual(windowLocationAssignStub.getCall(0).args, [url]); + assert.isFalse(pjaxClickStub.called); + + assert.equal(formSubmitsCount, 0); + assert.isFalse(pjaxSubmitStub.called); + assert.equal($('form').length, initialFormsCount); + } + + function verifyPageLoadWithPjax($element, event, pjaxContainerId) { + assert.isFalse(windowLocationAssignStub.called); + assert.isTrue(pjaxClickStub.calledOnce); + + assert.equal(formSubmitsCount, 0); + assert.isFalse(pjaxSubmitStub.called); + assert.equal($('form').length, initialFormsCount); + + assert.strictEqual(pjaxClickStub.getCall(0).args[0], event); + + var pjaxOptions = pjaxClickStub.getCall(0).args[1]; + + // container needs to be checked separately + + if (typeof pjaxOptions.container === 'string') { + assert.equal(pjaxOptions.container, '#' + pjaxContainerId || 'body'); + } else { + assert.instanceOf(pjaxOptions.container, $); + assert.equal(pjaxOptions.container.attr('id'), pjaxContainerId || 'body'); + } + delete pjaxOptions.container; + + assert.deepEqual(pjaxOptions, { + push: true, + replace: true, + scrollTo: 'scrollTo', + pushRedirect: 'pushRedirect', + replaceRedirect: 'replaceRedirect', + skipOuterContainers: 'skipOuterContainers', + timeout: 'timeout', + originalEvent: event, + originalTarget: $element + }); + } + + function verifyFormSubmit($form) { + assert.isFalse(windowLocationAssignStub.called); + assert.isFalse(pjaxClickStub.called); + + assert.equal(formSubmitsCount, 1); + assert.isFalse(pjaxSubmitStub.called); + assert.equal($('form').length, initialFormsCount); + + if ($form) { + assert.equal($form.attr('id'), $savedSubmittedForm.attr('id')); + } + } + + function verifyFormSubmitWithPjax($element, event, $form) { + assert.isFalse(windowLocationAssignStub.called); + assert.isFalse(pjaxClickStub.called); + + assert.equal(formSubmitsCount, 1); + assert.isTrue(pjaxSubmitStub.calledOnce); + assert.equal($('form').length, initialFormsCount); + + if ($form) { + assert.equal($form.attr('id'), $savedSubmittedForm.attr('id')); + } + + var pjaxEvent = pjaxSubmitStub.getCall(0).args[0]; + assert.instanceOf(pjaxEvent, $.Event); + assert.equal(pjaxEvent.type, 'submit'); + + var pjaxOptions = pjaxSubmitStub.getCall(0).args[1]; + + // container needs to be checked separately + + assert.instanceOf(pjaxOptions.container, $); + assert.equal(pjaxOptions.container.attr('id'), 'body'); + delete pjaxOptions.container; + + assert.deepEqual(pjaxOptions, { + push: true, + replace: true, + scrollTo: 'scrollTo', + pushRedirect: 'pushRedirect', + replaceRedirect: 'replaceRedirect', + skipOuterContainers: 'skipOuterContainers', + timeout: 'timeout', + originalEvent: event, + originalTarget: $element + }); + } + + describe('with no data-method', function () { + var noActionsMessage = 'should not do any actions related with page load and form submit'; + var pageLoadMessage = 'should load new page using the link from "href" attribute'; + var pageLoadWithPjaxMessage = pageLoadMessage + ' with pjax'; + + describe('with invalid elements or configuration', function () { + describe('with no form', function () { + withData({ + // Links + 'link, no href': ['.link-no-href'], + 'link, empty href': ['.link-empty-href'], + 'link, href contains anchor ("#") only': ['.link-anchor-href'], + 'link, no href, data-pjax': ['.link-no-href-pjax'], + 'link, empty href, data-pjax': ['.link-empty-href-pjax'], + 'link, href contains anchor ("#") only, data-pjax': ['.link-anchor-href-pjax'], + // Not links + 'not submit, no form': ['.not-submit-no-form'], + 'submit, no form': ['.submit-no-form'], + 'submit, data-form, form does not exist': ['.submit-form-not-exist'], + 'not submit, no form, data-pjax': ['.not-submit-no-form-pjax'], + 'submit, no form, data-pjax': ['.submit-no-form-pjax'], + 'submit, data-form, form does not exist, data-pjax': ['.submit-form-not-exist-pjax'] + }, function (elementSelector) { + it(noActionsMessage, function () { + var $element = $('.handle-action .no-method .invalid .no-form').find(elementSelector); + assert.lengthOf($element, 1); + + yii.handleAction($element); + verifyNoActions(); + }); + }); + }); + + describe('with form', function () { + withData({ + 'not submit, data-form': ['.not-submit-outside-form', '#not-submit-separate-form'], + 'not submit, inside a form': ['.not-submit-inside-form', '#not-submit-parent-form'], + 'not submit, data-form, data-pjax': [ + '.not-submit-outside-form-pjax', '#not-submit-separate-form' + ], + 'not submit, inside a form, data-pjax': [ + '.not-submit-inside-form-pjax', '#not-submit-parent-form-pjax' + ] + }, function (elementSelector, formSelector) { + it(noActionsMessage, function () { + var $element = $('.handle-action .no-method .invalid .form').find(elementSelector); + assert.lengthOf($element, 1); + + var $form = $(formSelector); + assert.lengthOf($form, 1); + + yii.handleAction($element); + verifyNoActions(); + }); + }); + }); + }); + + describe('with valid elements and configuration', function () { + describe('with no form', function () { + withData({ + 'link': ['.link'], + 'link, data-pjax="0"': ['.link-pjax-0'] + }, function (elementSelector) { + it(pageLoadMessage, function () { + var $element = $('.handle-action .no-method .valid').find(elementSelector); + assert.lengthOf($element, 1); + + yii.handleAction($element); + verifyPageLoad('/tests/index'); + }); + }); + + describe('with link, data-pjax and no pjax support', function () { + before(function () { + $.support.pjax = false; + }); + + after(function () { + $.support.pjax = true; + }); + + it(pageLoadMessage, function () { + var $element = $('.handle-action .no-method .valid .link-pjax'); + assert.lengthOf($element, 1); + + yii.handleAction($element); + verifyPageLoad('/tests/index'); + }); + }); + + withData({ + 'link, data-pjax': ['.link-pjax', 'body'], + 'link, data-pjax="1"': ['.link-pjax-1', 'body'], + 'link, data-pjax="true"': ['.link-pjax-true', 'body'], + 'link, data-pjax, outside a container': [ + '.link-pjax-outside-container', 'pjax-separate-container' + ], + 'link href, data-pjax, inside a container': ['.link-pjax-inside-container', 'pjax-container-2'] + }, function (elementSelector, expectedPjaxContainerId) { + it(pageLoadWithPjaxMessage, function () { + var event = $.Event('click'); + var $element = $('.handle-action .no-method .valid').find(elementSelector); + assert.lengthOf($element, 1); + + yii.handleAction($element, event); + verifyPageLoadWithPjax($element, event, expectedPjaxContainerId); + }); + }); + }); + + describe('with form', function () { + withData({ + 'submit, data-form': ['.submit-outside-form', '#submit-separate-form'], + 'submit, inside a form': ['.submit-inside-form', '#submit-parent-form'] + }, function (elementSelector, formSelector) { + it('should submit according existing form', function () { + var $element = $('.handle-action .no-method .valid').find(elementSelector); + assert.lengthOf($element, 1); + + var $form = $(formSelector); + var initialFormHtml = $form.get(0).outerHTML; + assert.lengthOf($form, 1); + + yii.handleAction($element); + + verifyFormSubmit($form); + assert.equal($savedSubmittedForm.get(0).outerHTML, initialFormHtml); + }); + }); + + withData({ + 'submit, data-form, data-pjax': ['.submit-outside-form-pjax', '#submit-separate-form'], + 'submit, inside a form, data-pjax': ['.submit-inside-form-pjax', '#submit-parent-form-pjax'] + }, function (elementSelector, formSelector) { + it('should submit according existing form with pjax', function () { + var event = $.Event('click'); + var $element = $('.handle-action .no-method .valid').find(elementSelector); + assert.lengthOf($element, 1); + + var $form = $(formSelector); + var initialFormHtml = $form.get(0).outerHTML; + assert.lengthOf($form, 1); + + yii.handleAction($element, event); + + verifyFormSubmitWithPjax($element, event, $form); + assert.equal($savedSubmittedForm.get(0).outerHTML, initialFormHtml); + }); + }); + }); + }); + }); + + describe('with data-method', function () { + describe('with no form', function () { + withData({ + 'invalid href': [ + '.bad-href', + '
' + ], + 'invalid data-params': [ + '.bad-params', + '
' + ], + 'data-method="get", data-params, target': [ + '.get-params-target', + '
' + + '' + + '' + + '
' + ], + 'data-method="head", data-params': [ + '.head', + '
' + + '' + + '' + + '' + + '' + + '
' + ], + 'data-method="post", data-params': [ + '.post', + '
' + + '' + + '' + + '' + + '
' + ], + 'data-method="post", data-params, upper case': [ + '.post-upper-case', + '
' + + '' + + '' + + '' + + '
' + ], + 'data-method="put", data-params': [ + '.put', + '
' + + '' + + '' + + '' + + '' + + '
' + ] + }, function (elementSelector, expectedFormHtml) { + it('should create temporary form and submit it', function () { + var $element = $('.handle-action .method .no-form').find(elementSelector); + assert.lengthOf($element, 1); + + yii.handleAction($element); + + verifyFormSubmit(); + assert.equal($savedSubmittedForm.get(0).outerHTML, expectedFormHtml); + }); + }); + + describe('with data-method="get", data-params, data-pjax', function () { + it('should create temporary form and submit it with pjax', function () { + var event = $.Event('click'); + var $element = $('.handle-action .method .no-form .get-params-pjax'); + assert.lengthOf($element, 1); + + yii.handleAction($element, event); + + verifyFormSubmitWithPjax($element, event); + + var expectedFormHtml = '
' + + '' + + '' + + '
'; + assert.equal($savedSubmittedForm.get(0).outerHTML, expectedFormHtml); + }); + }); + }); + + describe('with form', function () { + withData({ + 'data-form, new action, new method, data-params': [ + '.new-action-new-method', + '#method-form', + '
' + + '' + + '' + + '' + + '
' + ], + 'data-form, same action, same method, data-params': [ + '.same-action-same-method', + '#method-form', + '
' + + '' + + '' + + '' + + '
' + ], + 'data-form, invalid action, new method, data-params': [ + '.bad-action-new-method', + '#method-form', + '
' + + '' + + '' + + '' + + '
' + ], + // This is a test for this PR: + // https://github.com/yiisoft/yii2/pull/8014 + // + // However the bug currently can not be reproduced in jsdom: + // https://github.com/tmpvar/jsdom/issues/1688 + 'data-form, same action, same method, hidden "method" and "action" inputs in data-params': [ + '.hidden-method-action', + '#form-hidden-method-action', + '
' + + '' + + '' + + '' + + '' + + '' + + '
' + ] + }, function (elementSelector, formSelector, expectedSubmittedFormHtml) { + var message = 'should modify according existing form, submit it and restore to initial condition'; + it(message, function () { + var $element = $('.handle-action .method .form').find(elementSelector); + assert.lengthOf($element, 1); + + var $form = $(formSelector); + var initialFormHtml = $form.get(0).outerHTML; + assert.lengthOf($form, 1); + + $form.data('yiiActiveForm', {}); + + yii.handleAction($element); + + verifyFormSubmit($form); + + var submittedFormHtml = StringUtils.cleanHTML($savedSubmittedForm.get(0).outerHTML); + assert.equal(submittedFormHtml, expectedSubmittedFormHtml); + assert.equal($form.get(0).outerHTML, initialFormHtml); + + // When activeForm is used for this form, the element triggered the submit should be remembered + // in jQuery data under according key + assert.strictEqual($form.data('yiiActiveForm').submitObject, $element); + }); + }); + + describe('with data-form, new action, new method, data-params, data-pjax', function () { + var message = 'should modify according existing form, submit it with pjax and restore to ' + + ' initial condition'; + it(message, function () { + var event = $.Event('click'); + var $element = $('.handle-action .method .form .new-action-new-method-pjax'); + assert.lengthOf($element, 1); + + var $form = $('#method-form'); + var initialFormHtml = $form.get(0).outerHTML; + assert.lengthOf($form, 1); + + yii.handleAction($element, event); + + verifyFormSubmitWithPjax($element, event, $form); + + var expectedSubmittedFormHtml = '
' + + '' + + '' + + '' + + '
'; + var submittedFormHtml = StringUtils.cleanHTML($savedSubmittedForm.get(0).outerHTML); + assert.equal(submittedFormHtml, expectedSubmittedFormHtml); + assert.equal($form.get(0).outerHTML, initialFormHtml); + }); + }); + }); + }); + }); + + describe('getQueryParams method', function () { + withData({ + 'no query parameters': ['/posts/index', {}], + 'query parameters': ['/posts/index?foo=1&bar=2', {foo: '1', bar: '2'}], + 'query parameter with multiple values (not array)': ['/posts/index?foo=1&foo=2', {'foo': ['1', '2']}], + 'query parameter with multiple values (array)': ['/posts/index?foo[]=1&foo[]=2', {'foo[]': ['1', '2']}], + 'anchor': ['/posts/index#post', {}], + 'query parameters, anchor': ['/posts/index?foo=1&bar=2#post', {foo: '1', bar: '2'}], + 'relative url, query parameters': ['?foo=1&bar=2', {foo: '1', bar: '2'}], + 'relative url, anchor': ['#post', {}], + 'relative url, query parameters, anchor': ['?foo=1&bar=2#post', {foo: '1', bar: '2'}], + 'skipped parameter name': ['?foo=1&=2&baz=3#post', {foo: '1', baz: '3'}], + 'skipped values': [ + '?foo=&PostSearch[tags][]=1&PostSearch[tags][]=', {foo: '', 'PostSearch[tags][]': ['1', '']} + ], + 'encoded URI component': ['/posts/index?query=' + encodeURIComponent('count >= 1'), {query: 'count >= 1'}], + // https://github.com/yiisoft/yii2/issues/11921 + 'encoded URI component, "+" signs': [ + '/posts/index?next+celebration+day=Sunday+January+1st&' + + 'increase+' + encodeURIComponent('++') + '+' + encodeURIComponent('%') + + '=' + + encodeURIComponent('++') + '+20+' + encodeURIComponent('%'), + {'next celebration day': 'Sunday January 1st', 'increase ++ %': '++ 20 %'} + ], + 'multiple arrays, anchor': [ + '/posts/index?CategorySearch[id]=1&CategorySearch[name]=a' + + '&PostSearch[name]=b&PostSearch[category_id]=2&PostSearch[tags][]=3&PostSearch[tags][]=4' + + '&foo[]=5&foo[]=6&bar=7#post', + { + 'CategorySearch[id]': '1', + 'CategorySearch[name]': 'a', + 'PostSearch[name]': 'b', + 'PostSearch[category_id]': '2', + 'PostSearch[tags][]': ['3', '4'], + 'foo[]': ['5', '6'], + bar: '7' + } + ] + }, function (url, expectedParams) { + it('should parse all query parameters from string and return them within a object', function () { + assert.deepEqual(yii.getQueryParams(url), expectedParams); + }); + }); + }); + + describe('initModule method', function () { + var calledInitMethods = []; + var rootModuleInit = function () { + calledInitMethods.push('rootModule'); + }; + + afterEach(function () { + calledInitMethods = []; + }); + + withData({ + 'isActive is undefined in the root module': [ + undefined, + rootModuleInit, + ['rootModule', 'isActiveUndefined', 'isActiveTrue', 'subModule', 'subModule2'] + ], + 'isActive is true in the root module': [ + true, + rootModuleInit, + ['rootModule', 'isActiveUndefined', 'isActiveTrue', 'subModule', 'subModule2'] + ], + 'isActive is false in the root module': [false, rootModuleInit, []], + 'isActive is undefined in the root module, init is not a method': [ + undefined, + 'init', + ['isActiveUndefined', 'isActiveTrue', 'subModule', 'subModule2'] + ] + }, function (rootModuleIsActive, rootModuleInit, expectedCalledInitMethods) { + var message = 'should call init method in the root module and all submodules depending depending on ' + + 'activity and if init is a valid method'; + it(message, function () { + // Root module + + var module = (function () { + return { + isActive: rootModuleIsActive, + init: rootModuleInit + }; + })(); + + // Submodules + + module.isActiveUndefined = (function () { + return { + init: function () { + calledInitMethods.push('isActiveUndefined'); + } + }; + })(); + + module.isActiveTrue = (function () { + return { + isActive: true, + init: function () { + calledInitMethods.push('isActiveTrue'); + } + }; + })(); + + module.isActiveFalse = (function () { + return { + isActive: false, + init: function () { + calledInitMethods.push('isActiveFalse'); + } + }; + })(); + + module.initNotFunction = (function () { + return { + init: 'init' + }; + })(); + + module.someInteger = 1; + module.someString = 'string'; + + module.subModule = (function () { + return { + init: function () { + calledInitMethods.push('subModule'); + } + }; + })(); + + module.subModule.subModule2 = (function () { + return { + init: function () { + calledInitMethods.push('subModule2'); + } + }; + })(); + + yii.initModule(module); + assert.deepEqual(calledInitMethods, expectedCalledInitMethods); + }); + }); + }); + + describe('CSRF handler', function () { + var server; + var yiiGetCsrfParamStub; + var fakeCsrfParam; + + beforeEach(function () { + server = sinon.fakeServer.create(); + window.XMLHttpRequest = global.XMLHttpRequest; + yiiGetCsrfParamStub = sinon.stub(yii, 'getCsrfParam', function () { + return fakeCsrfParam; + }) + }); + + afterEach(function () { + server.restore(); + yiiGetCsrfParamStub.restore(); + }); + + withData({ + 'crossDomain is false, csrfParam is not set': [false, undefined, undefined], + 'crossDomain is false, csrfParam is set': [false, 'foobar', 'foobar'], + 'crossDomain is true, csrfParam is not set': [true, undefined, undefined], + 'crossDomain is true, csrfParam is set': [true, 'foobar', undefined] + }, function (crossDomain, csrfParam, expectedHeaderValue) { + var message = 'should add header with CSRF token to AJAX requests only when crossDomain is false and ' + + 'csrf parameter is set'; + it(message, function () { + fakeCsrfParam = csrfParam; + $.ajax({ + url: '/tests/index', + crossDomain: crossDomain + }); + server.requests[0].respond(200, {}, ''); + + assert.lengthOf(server.requests, 1); + assert.equal(server.requests[0].requestHeaders['X-CSRF-Token'], expectedHeaderValue); + }); + }); + }); + + describe('redirect handler', function () { + var windowLocationAssignStub; + + beforeEach(function () { + windowLocationAssignStub = sinon.stub(window.location, 'assign'); + }); + + afterEach(function () { + windowLocationAssignStub.restore(); + }); + + // https://github.com/yiisoft/yii2/pull/10974 + describe('with xhr undefined', function () { + it('should not perform redirect', function () { + var e = $.Event('ajaxComplete'); + $('body').trigger(e); + + assert.isFalse(windowLocationAssignStub.called); + }); + }); + + describe('with xhr defined', function () { + var server; + var response = {result: 'OK'}; + + beforeEach(function () { + server = sinon.fakeServer.create(); + window.XMLHttpRequest = global.XMLHttpRequest; + }); + + afterEach(function () { + server.restore(); + }); + + describe('with custom header not set', function () { + it('should not perform redirect', function () { + $.get('/tests/index'); + server.requests[0].respond(200, {}, ''); + + assert.lengthOf(server.requests, 1); + assert.isFalse(windowLocationAssignStub.called); + }); + }); + + describe('with custom header set', function () { + it('should perform redirect', function () { + $.get('/tests/index'); + server.requests[0].respond(200, {'X-Redirect': 'http://redirect.yii'}, ''); + + assert.lengthOf(server.requests, 1); + assert.isTrue(windowLocationAssignStub.calledOnce); + assert.deepEqual(windowLocationAssignStub.getCall(0).args, ['http://redirect.yii']); + }); + }); + }); + }); + + describe('asset filters', function () { + var server; + var ajaxDataType; + var jsResponse = { + status: 200, + headers: {'Content-Type': 'application/x-custom-javascript'}, + body: 'var foobar = 1;' + }; + + before(function () { + // Sent ajax requests with dataType "script" and "jsonp" are not captured by Sinon's fake server. + // As a workaround we can use custom dataType. + // This $.ajaxPrefilter handler must be run after the one from yii.js. + ajaxDataType = 'customscript'; + $.ajaxPrefilter('script', function () { + return ajaxDataType; + }); + $.ajaxSetup({ + accepts: { + customscript: 'application/x-custom-javascript' + }, + converters: { + 'text customscript': function (result) { + return result; + } + } + }); + }); + + beforeEach(function () { + server = sinon.fakeServer.create(); + // Allowed: /js/test.js, http://foo.bar/js/test.js + server.respondWith(/(http:\/\/foo\.bar)?\/js\/.+\.js/, [ + jsResponse.status, + jsResponse.headers, + jsResponse.body + ]); + window.XMLHttpRequest = global.XMLHttpRequest; + }); + + after(function () { + ajaxDataType = 'script'; + }); + + afterEach(function () { + server.restore(); + }); + + function respondToRequestWithSuccess(requestIndex) { + server.requests[requestIndex].respond(jsResponse.status, jsResponse.headers, jsResponse.body); + } + + function respondToRequestWithError(requestIndex) { + server.requests[requestIndex].respond(404, {}, ''); + } + + // Note: Please do not test loading of the script with the same name in different tests. After successful + // loading it will stay in loadedScripts and the load will be aborted unless this script is reloadable. + + describe('with scripts', function () { + var XHR_UNSENT; + var XHR_OPENED; + var XHR_DONE; + + before(function () { + XHR_UNSENT = window.XMLHttpRequest.UNSENT; + XHR_OPENED = window.XMLHttpRequest.OPENED; + XHR_DONE = window.XMLHttpRequest.DONE; + }); + + describe('with jsonp dataType', function () { + it('should load it as many times as it was requested', function () { + $.ajax({ + url: '/js/jsonp.js', + dataType: 'jsonp' + }); + server.respond(); + + $.ajax({ + url: '/js/jsonp.js', + dataType: 'jsonp' + }); + server.respond(); + + assert.lengthOf(server.requests, 2); + assert.equal(server.requests[0].readyState, XHR_DONE); + assert.equal(server.requests[1].readyState, XHR_DONE); + }); + }); + + describe('with scripts loaded on the page load', function () { + it('should prevent of loading them again for both relative and absolute urls', function () { + $.getScript('/js/existing1.js'); + server.respond(); + + $.getScript('http://foo.bar/js/existing1.js'); + server.respond(); + + $.getScript('/js/existing2.js'); + server.respond(); + + $.getScript('http://foo.bar/js/existing2.js'); + server.respond(); + + assert.lengthOf(server.requests, 0); + }); + }); + + describe('with script not loaded before', function () { + it('should load it only once for both relative and absolute urls', function () { + $.getScript('/js/new.js'); + server.respond(); + + $.getScript('/js/new.js'); + server.respond(); + + $.getScript('http://foo.bar/js/new.js'); + server.respond(); + + assert.lengthOf(server.requests, 1); + assert.equal(server.requests[0].readyState, XHR_DONE); + }); + }); + + describe('with reloadableScripts set', function () { + before(function () { + yii.reloadableScripts = [ + '/js/reloadable.js', + // https://github.com/yiisoft/yii2/issues/11494 + '/js/reloadable/script*.js' + ]; + }); + + after(function () { + yii.reloadableScripts = []; + }); + + describe('with match', function () { + withData({ + 'relative url, exact': ['/js/reloadable.js'], + 'relative url, wildcard': ['http://foo.bar/js/reloadable/script1.js'], + 'absolute url, exact': ['http://foo.bar/js/reloadable.js'], + 'absolute url, wildcard': ['http://foo.bar/js/reloadable/script2.js'] + }, function (url) { + it('should load it as many times as it was requested', function () { + $.getScript(url); + server.respond(); + + $.getScript(url); + server.respond(); + + assert.lengthOf(server.requests, 2); + assert.equal(server.requests[0].readyState, XHR_DONE); + assert.equal(server.requests[1].readyState, XHR_DONE); + }); + }); + }); + + describe('with no match', function () { + withData({ + 'relative url': ['/js/not_reloadable.js'], + 'absolute url': ['http://foo.bar/js/reloadable/not_reloadable_script.js'] + }, function (url) { + it('should load it only once for both relative and absolute urls', function () { + $.getScript(url); + server.respond(); + + $.getScript(url); + server.respond(); + + assert.lengthOf(server.requests, 1); + assert.equal(server.requests[0].readyState, XHR_DONE); + }); + }); + }); + + describe('with failed load after successful load and making it not reloadable', function () { + it('should allow to load it again', function () { + $.getScript('/js/reloadable/script_fail.js'); + respondToRequestWithSuccess(0); + + $.getScript('/js/reloadable/script_fail.js'); + respondToRequestWithError(1); + yii.reloadableScripts = []; + + $.getScript('/js/reloadable/script_fail.js'); + respondToRequestWithError(2); + + assert.lengthOf(server.requests, 3); + assert.equal(server.requests[0].readyState, XHR_DONE); + assert.equal(server.requests[1].readyState, XHR_DONE); + assert.equal(server.requests[2].readyState, XHR_DONE); + }); + }); + }); + + // https://github.com/yiisoft/yii2/issues/10358 + // https://github.com/yiisoft/yii2/issues/13307 + + describe('with concurrent requests', function () { + // Note: it's not possible to imitate successful loading of all requests, because after the first one + // loads, the rest will be aborted by yii.js (readyState will be 0 (UNSENT)). + // Sinon requires request to have state 1 (OPENED) for the response to be sent. + // Anyway one of the requests will be loaded at least a bit earlier than the others, so we can test + // that. + describe('with one successfully completed after one failed', function () { + it('should abort remaining requests and disallow to load the script again', function () { + $.getScript('/js/concurrent_success.js'); + $.getScript('/js/concurrent_success.js'); + $.getScript('/js/concurrent_success.js'); + + assert.lengthOf(server.requests, 3); + + assert.equal(server.requests[0].readyState, XHR_OPENED); + assert.equal(server.requests[1].readyState, XHR_OPENED); + assert.equal(server.requests[2].readyState, XHR_OPENED); + + respondToRequestWithError(2); + respondToRequestWithSuccess(1); + + assert.equal(server.requests[0].readyState, XHR_UNSENT); + assert.isTrue(server.requests[0].aborted); + assert.equal(server.requests[1].readyState, XHR_DONE); + assert.isUndefined(server.requests[1].aborted); + assert.equal(server.requests[2].readyState, XHR_DONE); + assert.isUndefined(server.requests[2].aborted); + + $.getScript('/js/concurrent_success.js'); + server.respond(); + + assert.lengthOf(server.requests, 3); + }); + }); + + describe('with all requests failed', function () { + it('should allow to load the script again', function () { + $.getScript('/js/concurrent_fail.js'); + $.getScript('/js/concurrent_fail.js'); + $.getScript('/js/concurrent_fail.js'); + + respondToRequestWithError(0); + respondToRequestWithError(1); + respondToRequestWithError(2); + + $.getScript('/js/concurrent_fail.js'); + + respondToRequestWithSuccess(3); + + assert.lengthOf(server.requests, 4); + assert.equal(server.requests[0].readyState, XHR_DONE); + assert.equal(server.requests[1].readyState, XHR_DONE); + assert.equal(server.requests[2].readyState, XHR_DONE); + assert.equal(server.requests[3].readyState, XHR_DONE); + }); + }); + + describe('with requests to different urls successfully completed', function () { + it('should not cause any conflicts and disallow to load these scripts again', function () { + $.getScript('/js/concurrent_url1.js'); + $.getScript('/js/concurrent_url2.js'); + + $.getScript('/js/concurrent_url1.js'); + $.getScript('/js/concurrent_url2.js'); + + respondToRequestWithSuccess(0); + respondToRequestWithSuccess(3); + + $.getScript('/js/concurrent_url1.js'); + $.getScript('/js/concurrent_url2.js'); + + assert.lengthOf(server.requests, 4); + assert.equal(server.requests[0].readyState, XHR_DONE); + assert.isUndefined(server.requests[0].aborted); + assert.equal(server.requests[1].readyState, XHR_UNSENT); + assert.isTrue(server.requests[1].aborted); + assert.equal(server.requests[2].readyState, XHR_UNSENT); + assert.isTrue(server.requests[2].aborted); + assert.equal(server.requests[3].readyState, XHR_DONE); + assert.isUndefined(server.requests[3].aborted); + }); + }); + }); + }); + + describe('with stylesheets', function () { + // Note: All added stylesheets for the tests must have ".added-stylesheet" class for the proper cleanup + + afterEach(function () { + $('.added-stylesheet').remove(); + }); + + describe('with not reloadable assets', function () { + it('should not allow to add duplicate stylesheets for both relative and absolute urls', function () { + var $styleSheets = $('.asset-filters .stylesheets'); + assert.lengthOf($styleSheets, 1); + + $.get('/tests/index', function () { + $styleSheets.append( + '' + ); + $styleSheets.append( + '' + ); + }); + + server.requests[0].respond(200, {}, ''); + + assert.lengthOf($('link[rel="stylesheet"]'), 2); + assert.lengthOf($('#stylesheet1'), 1); + assert.lengthOf($('#stylesheet2'), 1); + }); + }); + + describe('with reloadable assets', function () { + before(function () { + yii.reloadableScripts = [ + '/css/reloadable.css', + // https://github.com/yiisoft/yii2/issues/11494 + '/css/reloadable/stylesheet*.css' + ]; + }); + + after(function () { + yii.reloadableScripts = []; + }); + + it('should allow to add duplicate stylesheets for both relative and absolute urls', function () { + var $styleSheets = $('.asset-filters .stylesheets'); + assert.lengthOf($styleSheets, 1); + + $.get('/tests/index', function () { + $styleSheets.append( + '' + ); + $styleSheets.append( + '' + ); + $styleSheets.append( + '' + ); + $styleSheets.append( + '' + ); + }); + + server.requests[0].respond(200, {}, ''); + + assert.lengthOf($('link[rel=stylesheet]'), 6); + }); + }); + }); + }); + + describe('data methods', function () { + var windowConfirmStub; + var yiiConfirmSpy; + var yiiHandleActionStub; + var extraEventHandlerSpy; + + beforeEach(function () { + windowConfirmStub = sinon.stub(window, 'confirm', function () { + return true; + }); + yiiConfirmSpy = sinon.spy(yii, 'confirm'); + yiiHandleActionStub = sinon.stub(yii, 'handleAction'); + extraEventHandlerSpy = sinon.spy(); + $(window.document).on('click change', '.data-methods-element', extraEventHandlerSpy); + }); + + afterEach(function () { + windowConfirmStub.restore(); + yiiConfirmSpy.restore(); + yiiHandleActionStub.restore(); + extraEventHandlerSpy.reset(); + $(window.document).off('click change', '.data-methods-element'); + }); + + describe('with data not set', function () { + it('should continue handling interaction with element', function () { + var event = $.Event('click'); + var $element = $('#data-methods-no-data'); + assert.lengthOf($element, 1); + + $element.trigger(event); + + assert.isFalse(yiiConfirmSpy.called); + assert.isFalse(yiiHandleActionStub.called); + assert.isTrue(extraEventHandlerSpy.calledOnce); + }); + }); + + describe('with clickableSelector with data-confirm', function () { + it('should call confirm and handleAction methods', function () { + var event = $.Event('click'); + var elementId = 'data-methods-click-confirm'; + var $element = $('#' + elementId); + assert.lengthOf($element, 1); + + $element.trigger(event); + + assert.isTrue(yiiConfirmSpy.calledOnce); + assert.equal(yiiConfirmSpy.getCall(0).args[0], 'Are you sure?'); + assert.isFunction(yiiConfirmSpy.getCall(0).args[1]); + // https://github.com/yiisoft/yii2/issues/10097 + assert.instanceOf(yiiConfirmSpy.getCall(0).thisValue, window.HTMLAnchorElement); + assert.equal(yiiConfirmSpy.getCall(0).thisValue.id, elementId); + + assert.isTrue(yiiHandleActionStub.calledOnce); + assert.equal(yiiHandleActionStub.getCall(0).args[0].attr('id'), elementId); + assert.strictEqual(yiiHandleActionStub.getCall(0).args[1], event); + + assert.isFalse(extraEventHandlerSpy.called); + }); + }); + + describe('with changeableSelector without data-confirm', function () { + var elementId = 'data-methods-change'; + var $element; + + before(function () { + $element = $('#' + elementId); + }); + + after(function () { + $element.val(''); + }); + + it('should call handleAction method only', function () { + var event = $.Event('change'); + assert.lengthOf($element, 1); + + $element.val(1); + $element.trigger(event); + + assert.isFalse(yiiConfirmSpy.called); + + assert.isTrue(yiiHandleActionStub.calledOnce); + assert.equal(yiiHandleActionStub.getCall(0).args[0].attr('id'), elementId); + assert.strictEqual(yiiHandleActionStub.getCall(0).args[1], event); + + assert.isFalse(extraEventHandlerSpy.called); + }); + }); + }); +}); From 7d12ae80ee7c2c2f7c1b04c73caba5610abfd05b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 25 Jan 2017 21:39:01 +0300 Subject: [PATCH 02/80] Added info about using your own forks when contributing to extensions and apps --- docs/internals/git-workflow.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/internals/git-workflow.md b/docs/internals/git-workflow.md index 1aa9e1f..f4f822e 100644 --- a/docs/internals/git-workflow.md +++ b/docs/internals/git-workflow.md @@ -47,11 +47,12 @@ If you are going to work with JavaScript: > Note: JavaScript tests depend on [jsdom](https://github.com/tmpvar/jsdom) library which requires Node.js 4 or newer. Using of Node.js 6 or 7 is more preferable. -- run `php build/build dev/app basic` to clone the basic app and install composer dependencies for the basic app. +- run `php build/build dev/app basic ` to clone the basic app and install composer dependencies for the basic app. + `` is URL of your repository fork such as `git@github.com:my_nickname/yii2-app-basic.git`. If you are core framework contributor you may skip specifying fork. This command will install foreign composer packages as normal but will link the yii2 repo to the currently checked out repo, so you have one instance of all the code installed. - Do the same for the advanced app if needed: `php build/build dev/app advanced`. + Do the same for the advanced app if needed: `php build/build dev/app advanced `. This command will also be used to update dependencies, it runs `composer update` internally. @@ -84,14 +85,14 @@ additional arguments). To work on extensions you have to clone the extension repository. We have created a command that can do this for you: ``` -php build/build dev/ext +php build/build dev/ext ``` -where `` is the name of the extension, e.g. `redis`. +where `` is the name of the extension, e.g. `redis` and `` is URL of your extension fork such as `git@github.com:my_nickname/yii2-redis.git`. If you are core framework contributor you may skip specifying fork. If you want to test the extension in one of the application templates, just add it to the `composer.json` of the application as you would normally do e.g. add `"yiisoft/yii2-redis": "~2.0.0"` to the `require` section of the basic app. -Running `php build/build dev/app basic` will install the extension and its dependencies and create +Running `php build/build dev/app basic ` will install the extension and its dependencies and create a symlink to `extensions/redis` so you are not working in the composer vendor dir but in the yii2 repository directly. > Note: The default git repository Urls clone from github via SSH, you may add the `--useHttp` flag to the `build` command From a0c5fee93141fd56bdd1b72110718d64de5c6418 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 27 Jan 2017 09:55:41 +0100 Subject: [PATCH 03/80] Revert ignoring tests and docs on archive export This did break the use case of installing the `yiisoft/yii2-dev` package for testing. `yiisoft/yii2` is a subsplit with minimal number of files of the framework already, the dev package should contain all tests and docs. See for example: tom--/yii2-dynamic-ar#18 Partially reverts https://github.com/yiisoft/yii2/commit/392ea93d797e8d9fc1d21aa2587aca51552a98fc --- .gitattributes | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitattributes b/.gitattributes index feef00d..5fb509b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,17 +22,15 @@ *.gif binary *.ttf binary -# Ignore all test and documentation for archive +# Ignore some meta files when creating an archive of this repository +# We do not ignore any content, because this repo represents the +# `yiisoft/yii2-dev` package, which is expected to ship all tests and docs. /.github export-ignore /.editorconfig export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.scrutinizer.yml export-ignore /.travis.yml export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore -/docs export-ignore -/build export-ignore # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ From 9deb24e262751c612a3cbd2fcdf8437c9e354d99 Mon Sep 17 00:00:00 2001 From: Nobuo Kihara Date: Fri, 27 Jan 2017 23:00:17 +0900 Subject: [PATCH 04/80] docs/guide/output-client-scripts.md typo fix [ci skip] (#13455) --- docs/guide/output-client-scripts.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/output-client-scripts.md b/docs/guide/output-client-scripts.md index 18f9e44..2b688f1 100644 --- a/docs/guide/output-client-scripts.md +++ b/docs/guide/output-client-scripts.md @@ -46,7 +46,7 @@ instead of adding a new one. If you don't provide it, the JS code itself will be ### Registering script files The arguments for [[yii\web\View::registerJsFile()|registerJsFile()]] are similar to those for -[[yii\web\View::registerCssFile()|registerCssFile()]]. In the above example, +[[yii\web\View::registerCssFile()|registerCssFile()]]. In the following example, we register the `main.js` file with the dependency on the [[yii\web\JqueryAsset]]. It means that the `main.js` file will be added AFTER `jquery.js`. Without such dependency specification, the relative order between `main.js` and `jquery.js` would be undefined and the code would not work. @@ -67,7 +67,7 @@ multiple JS files, which is desirable for high traffic websites. ## Registering CSS -Similar to Javascript, you can register CSS using +Similar to JavaScript, you can register CSS using [[yii\web\View::registerCss()|registerCss()]] or [[yii\web\View::registerCssFile()|registerCssFile()]]. The former registers a block of CSS code while the latter registers an external CSS file. @@ -170,7 +170,7 @@ variable definition, e.g.: var yiiOptions = {"appName":"My Yii Application","baseUrl":"/basic/web","language":"en"}; ``` -In your Javascript code you can now access these like `yiiOptions.baseUrl` or `yiiOptions.language`. +In your JavaScript code you can now access these like `yiiOptions.baseUrl` or `yiiOptions.language`. ### Passing translated messages @@ -189,7 +189,7 @@ JS ``` The above example code uses PHP -[Heredoc syntax](http://php.net/manual/de/language.types.string.php#language.types.string.syntax.heredoc) for better readability. This also enables better syntax highlighting in most IDEs so it is the +[Heredoc syntax](http://php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc) for better readability. This also enables better syntax highlighting in most IDEs so it is the preferred way of writing inline JavaScript, especially useful for code that is longer than a single line. The variable `$message` is created in PHP and thanks to [[yii\helpers\Json::htmlEncode|Json::htmlEncode]] it contains the string in valid JS syntax, which can be inserted into the JavaScript code to place the dynamic string in the function call to `alert()`. From f20c0177afc4dc5a4521adb57c7684eb67780335 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 27 Jan 2017 18:56:22 +0300 Subject: [PATCH 05/80] Fixes #7435: Added `EVENT_BEFORE_RUN`, `EVENT_AFTER_RUN` and corresponding methods to `yii\base\Widget` --- framework/CHANGELOG.md | 1 + framework/base/Widget.php | 85 +++++++++++++++++++++++++++++++++++++++++- framework/base/WidgetEvent.php | 46 +++++++++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 framework/base/WidgetEvent.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index fee219d..d126fa8 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -115,6 +115,7 @@ Yii Framework 2 Change Log - Bug #13401: Fixed lack of escaping of request dump at exception screens (samdark) - Enh #13417: Allow customizing `yii\data\ActiveDataProvider` in `yii\rest\IndexAction` (leandrogehlen) - Bug #12599: Fixed MSSQL fail to work with `nvarbinary`. Enhanced SQL scripts compatibility with older versions (samdark) +- Enh #7435: Added `EVENT_BEFORE_RUN`, `EVENT_AFTER_RUN` and corresponding methods to `yii\base\Widget` (petrabarus) 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/base/Widget.php b/framework/base/Widget.php index cb162b8..cadcd8f 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -27,6 +27,18 @@ use ReflectionClass; class Widget extends Component implements ViewContextInterface { /** + * @event WidgetEvent an event raised right before executing a widget. + * You may set [[WidgetEvent::isValid]] to be false to cancel the widget execution. + * @since 2.0.11 + */ + const EVENT_BEFORE_RUN = 'beforeRun'; + /** + * @event WidgetEvent an event raised right after executing a widget. + * @since 2.0.11 + */ + const EVENT_AFTER_RUN = 'afterRun'; + + /** * @var int a counter used to generate [[id]] for widgets. * @internal */ @@ -76,7 +88,12 @@ class Widget extends Component implements ViewContextInterface if (!empty(static::$stack)) { $widget = array_pop(static::$stack); if (get_class($widget) === get_called_class()) { - echo $widget->run(); + /* @var $widget Widget */ + if ($widget->beforeRun()) { + $result = $widget->run(); + $result = $widget->afterRun($result); + echo $result; + } return $widget; } else { throw new InvalidCallException('Expecting end() of ' . get_class($widget) . ', found ' . get_called_class()); @@ -101,7 +118,11 @@ class Widget extends Component implements ViewContextInterface /* @var $widget Widget */ $config['class'] = get_called_class(); $widget = Yii::createObject($config); - $out = $widget->run(); + $out = ''; + if ($widget->beforeRun()) { + $result = $widget->run(); + $out = $widget->afterRun($result); + } } catch (\Exception $e) { // close the output buffer opened above if it has not been closed already if (ob_get_level() > 0) { @@ -220,4 +241,64 @@ class Widget extends Component implements ViewContextInterface return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; } + + /** + * This method is invoked right before the widget is executed. + * + * The method will trigger the [[EVENT_BEFORE_RUN]] event. The return value of the method + * will determine whether the widget should continue to run. + * + * When overriding this method, make sure you call the parent implementation like the following: + * + * ```php + * public function beforeRun() + * { + * if (!parent::beforeRun()) { + * return false; + * } + * + * // your custom code here + * + * return true; // or false to not run the widget + * } + * ``` + * + * @return boolean whether the widget should continue to be executed. + * @since 2.0.11 + */ + public function beforeRun() + { + $event = new WidgetEvent($this); + $this->trigger(self::EVENT_BEFORE_RUN, $event); + return $event->isValid; + } + + /** + * This method is invoked right after a widget is executed. + * + * The method will trigger the [[EVENT_AFTER_RUN]] event. The return value of the method + * will be used as the widget return value. + * + * If you override this method, your code should look like the following: + * + * ```php + * public function afterRun($result) + * { + * $result = parent::afterRun($result); + * // your custom code here + * return $result; + * } + * ``` + * + * @param mixed $result the widget return result. + * @return mixed the processed widget result. + * @since 2.0.11 + */ + public function afterRun($result) + { + $event = new WidgetEvent($this); + $event->result = $result; + $this->trigger(self::EVENT_BEFORE_RUN, $event); + return $event->result; + } } diff --git a/framework/base/WidgetEvent.php b/framework/base/WidgetEvent.php new file mode 100644 index 0000000..2c6eda1 --- /dev/null +++ b/framework/base/WidgetEvent.php @@ -0,0 +1,46 @@ + + * @since 2.0.11 + */ +class WidgetEvent extends Event { + + /** + * @var Widget the widget currently being executed + */ + public $widget; + /** + * @var mixed the widget result. Event handlers may modify this property to change the widget result. + */ + public $result; + /** + * @var boolean whether to continue running the widget. Event handlers of + * [[Widget::EVENT_BEFORE_RUN]] may set this property to decide whether + * to continue running the current widget. + */ + public $isValid = true; + + + /** + * Constructor. + * @param Widget $widget the widget associated with this widget event. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($widget, $config = []) + { + $this->widget = $widget; + parent::__construct($config); + } +} From be4ebdd04926fd62c2afa2a87c107afd8d2f263c Mon Sep 17 00:00:00 2001 From: Dmitriy Bashkarev Date: Fri, 27 Jan 2017 19:03:45 +0300 Subject: [PATCH 06/80] Fixes #13134: Added logging URL rules (bashkarev) --- framework/CHANGELOG.md | 1 + framework/rest/UrlRule.php | 12 +++++-- framework/web/CompositeUrlRule.php | 12 +++++-- framework/web/UrlManager.php | 10 +++++- framework/web/UrlRule.php | 20 +++++++++++ tests/framework/web/UrlRuleTest.php | 71 +++++++++++++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 7 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index d126fa8..1b94aad 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -108,6 +108,7 @@ Yii Framework 2 Change Log - Enh #13266: Added `yii\validators\EachValidator::$stopOnFirstError` allowing addition of more than one error (klimov-paul) - Enh #13268: Added logging of memory usage (bashkarev) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) +- Enh #13134: Added logging URL rules (bashkarev) - Enh: Refactored `yii\web\ErrorAction` to make it reusable (silverfire) - Enh: Added support for field `yii\console\controllers\BaseMigrateController::$migrationNamespaces` setup from CLI (schmunk42) - Bug #13287: Fixed translating "and" separator in `UniqueValidator` error message (jetexe) diff --git a/framework/rest/UrlRule.php b/framework/rest/UrlRule.php index a4464c8..96bfbed 100644 --- a/framework/rest/UrlRule.php +++ b/framework/rest/UrlRule.php @@ -221,9 +221,15 @@ class UrlRule extends CompositeUrlRule if (strpos($pathInfo, $urlName) !== false) { foreach ($rules as $rule) { /* @var $rule \yii\web\UrlRule */ - if (($result = $rule->parseRequest($manager, $request)) !== false) { - Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); - + $result = $rule->parseRequest($manager, $request); + if (YII_DEBUG) { + Yii::trace([ + 'rule' => method_exists($rule, '__toString') ? $rule->__toString() : get_class($rule), + 'match' => $result !== false, + 'parent' => self::className() + ], __METHOD__); + } + if ($result !== false) { return $result; } } diff --git a/framework/web/CompositeUrlRule.php b/framework/web/CompositeUrlRule.php index 997a0ae..1fa25a2 100644 --- a/framework/web/CompositeUrlRule.php +++ b/framework/web/CompositeUrlRule.php @@ -47,9 +47,15 @@ abstract class CompositeUrlRule extends Object implements UrlRuleInterface { foreach ($this->rules as $rule) { /* @var $rule \yii\web\UrlRule */ - if (($result = $rule->parseRequest($manager, $request)) !== false) { - Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); - + $result = $rule->parseRequest($manager, $request); + if (YII_DEBUG) { + Yii::trace([ + 'rule' => method_exists($rule, '__toString') ? $rule->__toString() : get_class($rule), + 'match' => $result !== false, + 'parent' => self::className() + ], __METHOD__); + } + if ($result !== false) { return $result; } } diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 87f2a72..81faf68 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -263,7 +263,15 @@ class UrlManager extends Component if ($this->enablePrettyUrl) { /* @var $rule UrlRule */ foreach ($this->rules as $rule) { - if (($result = $rule->parseRequest($this, $request)) !== false) { + $result = $rule->parseRequest($this, $request); + if (YII_DEBUG) { + Yii::trace([ + 'rule' => method_exists($rule, '__toString') ? $rule->__toString() : get_class($rule), + 'match' => $result !== false, + 'parent' => null + ], __METHOD__); + } + if ($result !== false) { return $result; } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index ef065bd..a36e42a 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -126,6 +126,26 @@ class UrlRule extends Object implements UrlRuleInterface */ private $_routeParams = []; + /** + * @return string + * @since 2.0.11 + */ + public function __toString() + { + $str = ''; + if ($this->verb !== null) { + $str .= implode(',', $this->verb) . ' '; + } + if ($this->host !== null && strrpos($this->name, $this->host) === false) { + $str .= $this->host . '/'; + } + $str .= $this->name; + + if ($str === '') { + return '/'; + } + return $str; + } /** * Initializes this rule. diff --git a/tests/framework/web/UrlRuleTest.php b/tests/framework/web/UrlRuleTest.php index eb7b6c4..9051d4e 100644 --- a/tests/framework/web/UrlRuleTest.php +++ b/tests/framework/web/UrlRuleTest.php @@ -272,6 +272,16 @@ class UrlRuleTest extends TestCase $this->assertEquals(['post/index', ['page' => 1, 'tag' => 'a']], $result); } + public function testToString() + { + $suites = $this->getTestsForToString(); + foreach ($suites as $i => $suite) { + list ($name, $config, $test) = $suite; + $rule = new UrlRule($config); + $this->assertEquals($rule->__toString(), $test, "Test#$i: $name"); + } + } + protected function getTestsForCreateUrl() { // structure of each test @@ -1029,4 +1039,65 @@ class UrlRuleTest extends TestCase ], ]; } + + protected function getTestsForToString() + { + return [ + [ + 'empty pattern', + [ + 'pattern' => '', + 'route' => 'post/index', + ], + '/' + ], + [ + 'multiple params with special chars', + [ + 'pattern' => 'post///', + 'route' => 'post/index', + ], + 'post///' + ], + [ + 'with host info', + [ + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + 'host' => 'http://.example.com', + ], + 'http://.example.com/post//' + ], + [ + 'with host info in pattern', + [ + 'pattern' => 'http://.example.com/post//', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + 'http://.example.com/post//' + ], + [ + 'with verb', + [ + 'verb' => ['POST'], + 'pattern' => 'post/', + 'route' => 'post/index' + ], + 'POST post/' + ], + [ + 'with verbs', + [ + 'verb' => ['PUT', 'POST'], + 'pattern' => 'post/', + 'route' => 'post/index' + ], + 'PUT,POST post/' + ], + + + ]; + } } From bede9feba5b39169381d7af3c48ae89753fbe9e5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Jan 2017 01:13:22 +0300 Subject: [PATCH 07/80] Fixes #13453: Reverted #10896 commit 729ddc5b76e8159d051e9c5783f5534eeb212efb It was causing side effects: https://github.com/yiisoft/yii2/issues/13453 --- framework/CHANGELOG.md | 2 +- framework/validators/UniqueValidator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1b94aad..2c74f3c 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -63,7 +63,6 @@ Yii Framework 2 Change Log - Enh #7820: Add `or` relation for `targetAttribute` in `yii\validators\UniqueValidator` (developeruz) - Enh #9053: Added`yii\grid\RadioButtonColumn` (darwinisgod) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) -- Enh #10896: Select only primary key when counting records in UniqueValidator (developeruz) - Enh #10970: Allow omit specifying empty default params on URL creation (rob006) - Enh #11037: `yii.js` and `yii.validation.js` use `Regexp.test()` instead of `String.match()` (arogachev, nkovacs) - Enh #11163: Added separate method for client-side validation options `yii\validators\Validator::getClientOptions()` (arogachev) @@ -118,6 +117,7 @@ Yii Framework 2 Change Log - Bug #12599: Fixed MSSQL fail to work with `nvarbinary`. Enhanced SQL scripts compatibility with older versions (samdark) - Enh #7435: Added `EVENT_BEFORE_RUN`, `EVENT_AFTER_RUN` and corresponding methods to `yii\base\Widget` (petrabarus) + 2.0.10 October 20, 2016 ----------------------- diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 3fe522c..ad7003a 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -161,7 +161,7 @@ class UniqueValidator extends Validator } else { // if current $model is in the database already we can't use exists() /** @var $models ActiveRecordInterface[] */ - $models = $query->select($targetClass::primaryKey())->limit(2)->all(); + $models = $query->limit(2)->all(); $n = count($models); if ($n === 1) { $keys = array_keys($conditions); From 767400da199f93994e9e181960d973744256828e Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Jan 2017 01:22:48 +0300 Subject: [PATCH 08/80] Fixes for #13457 as commented by @cebe --- framework/base/Widget.php | 2 +- framework/base/WidgetEvent.php | 20 ++------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/framework/base/Widget.php b/framework/base/Widget.php index cadcd8f..62ba2d0 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -298,7 +298,7 @@ class Widget extends Component implements ViewContextInterface { $event = new WidgetEvent($this); $event->result = $result; - $this->trigger(self::EVENT_BEFORE_RUN, $event); + $this->trigger(self::EVENT_AFTER_RUN, $event); return $event->result; } } diff --git a/framework/base/WidgetEvent.php b/framework/base/WidgetEvent.php index 2c6eda1..d8582ad 100644 --- a/framework/base/WidgetEvent.php +++ b/framework/base/WidgetEvent.php @@ -15,12 +15,8 @@ namespace yii\base; * @author Petra Barus * @since 2.0.11 */ -class WidgetEvent extends Event { - - /** - * @var Widget the widget currently being executed - */ - public $widget; +class WidgetEvent extends Event +{ /** * @var mixed the widget result. Event handlers may modify this property to change the widget result. */ @@ -31,16 +27,4 @@ class WidgetEvent extends Event { * to continue running the current widget. */ public $isValid = true; - - - /** - * Constructor. - * @param Widget $widget the widget associated with this widget event. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($widget, $config = []) - { - $this->widget = $widget; - parent::__construct($config); - } } From 7c1693479fbc8441d1565958942aeb7490872cb6 Mon Sep 17 00:00:00 2001 From: user57376 Date: Thu, 21 Jul 2016 21:57:56 +0200 Subject: [PATCH 09/80] Fixes #12000: Added EVENT_INIT to widget --- framework/CHANGELOG.md | 4 ++++ framework/base/Widget.php | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 2c74f3c..69e2036 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -210,6 +210,10 @@ Yii Framework 2 Change Log - Enh #12710: Added `beforeItem` and `afterItem` to `yii\widgets\ListView` (mdmunir, silverfire) - Enh #12727: Enhanced `yii\widgets\Menu` to allow item option `active` be a Closure (voskobovich, silverfire) - Enh: Method `yii\console\controllers\AssetController::getAssetManager()` automatically enables `yii\web\AssetManager::forceCopy` in case it is not explicitly specified (pana1990, klimov-paul) +- Bug #11949: Fixed `ActiveField::end` generates close tag when it's `option['tag']` is null (egorio) +- Enh #11950: Improve BaseArrayHelper::keyExists speed (egorio) +- Bug #11972: Fixed active form `afterValidate` wasn't triggered in some cases (lynicidn) +- Enh #12000: Added EVENT_INIT to widget (user57376) 2.0.9 July 11, 2016 ------------------- diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 62ba2d0..713df37 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -27,6 +27,11 @@ use ReflectionClass; class Widget extends Component implements ViewContextInterface { /** + * @event Event an event that is triggered when the widget is initialized via [[init()]]. + * @since 2.0.11 + */ + const EVENT_INIT = 'init'; + /** * @event WidgetEvent an event raised right before executing a widget. * You may set [[WidgetEvent::isValid]] to be false to cancel the widget execution. * @since 2.0.11 @@ -55,6 +60,16 @@ class Widget extends Component implements ViewContextInterface */ public static $stack = []; + /** + * Initializes the object. + * This method is called at the end of the constructor. + * The default implementation will trigger an [[EVENT_INIT]] event. + */ + public function init() + { + parent::init(); + $this->trigger(self::EVENT_INIT); + } /** * Begins a widget. From 8803496cf59c2433308c69d9a7ad23291632754c Mon Sep 17 00:00:00 2001 From: Nobuo Kihara Date: Sat, 28 Jan 2017 17:45:51 +0900 Subject: [PATCH 10/80] docs/guide/output-data-widgets.md typo fix [ci skip] (#13459) --- docs/guide/output-data-widgets.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/output-data-widgets.md b/docs/guide/output-data-widgets.md index 2afbccc..8408be1 100644 --- a/docs/guide/output-data-widgets.md +++ b/docs/guide/output-data-widgets.md @@ -29,8 +29,8 @@ echo DetailView::widget([ [ // the owner name of the model 'label' => 'Owner', 'value' => $model->owner->name, - 'contentOptions' => ['class' => 'bg-red'], // to HTML customize attributes of value tag - 'captionOptions' => ['tooltip' => 'Tooltip'], // to HTML customize attributes of label tag + 'contentOptions' => ['class' => 'bg-red'], // HTML attributes to customize value tag + 'captionOptions' => ['tooltip' => 'Tooltip'], // HTML attributes to customize label tag ], 'created_at:datetime', // creation date formatted as datetime ], @@ -38,8 +38,8 @@ echo DetailView::widget([ ``` Remember that unlike [[yii\widgets\GridView|GridView]] which processes a set of models, -[[yii\widgets\DetailView|DetailView]] processes just one. So most of the times there is no need for using closure since -`$model` is the only one model for display and available in view as variable. +[[yii\widgets\DetailView|DetailView]] processes just one. So most of the time there is no need for using closure since +`$model` is the only one model for display and available in view as a variable. However some cases can make using of closure useful. For example when `visible` is specified and you want to prevent `value` calculations in case it evaluates to `false`: @@ -600,7 +600,7 @@ $query->andFilterWhere(['LIKE', 'author.name', $this->getAttribute('author.name' > $query->andFilterWhere(['LIKE', 'au.name', $this->getAttribute('author.name')]); > ``` > -> The same is `true` for the sorting definition: +> The same is true for the sorting definition: > > ```php > $dataProvider->sort->attributes['author.name'] = [ From 920877c815313ad9dfacb5bb34f22d2dca3340ab Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Jan 2017 11:54:57 +0300 Subject: [PATCH 11/80] Fixed WidgetEvent construction --- framework/base/Widget.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 713df37..e19e7b3 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -283,7 +283,7 @@ class Widget extends Component implements ViewContextInterface */ public function beforeRun() { - $event = new WidgetEvent($this); + $event = new WidgetEvent(); $this->trigger(self::EVENT_BEFORE_RUN, $event); return $event->isValid; } @@ -311,7 +311,7 @@ class Widget extends Component implements ViewContextInterface */ public function afterRun($result) { - $event = new WidgetEvent($this); + $event = new WidgetEvent(); $event->result = $result; $this->trigger(self::EVENT_AFTER_RUN, $event); return $event->result; From 1cc327f108d83b10e50ae2f364dd057cd76ac489 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Jan 2017 14:41:57 +0300 Subject: [PATCH 12/80] Run common batch insert tests for SQLite --- tests/framework/db/sqlite/QueryBuilderTest.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/framework/db/sqlite/QueryBuilderTest.php b/tests/framework/db/sqlite/QueryBuilderTest.php index ec1934a..551f787 100644 --- a/tests/framework/db/sqlite/QueryBuilderTest.php +++ b/tests/framework/db/sqlite/QueryBuilderTest.php @@ -59,7 +59,14 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest $this->markTestSkipped('Comments are not supported in SQLite'); } - public function testBatchInsert() + public function batchInsertProvider() + { + $data = parent::batchInsertProvider(); + $data['escape-danger-chars']['expected'] = "INSERT INTO `customer` (`address`) VALUES ('SQL-danger chars are escaped: ''); --')"; + return $data; + } + + public function testBatchInsertOnOlderVersions() { $db = $this->getConnection(); if (version_compare($db->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.7.11', '>=')) { From 015f14e37417defd811ad3d281a40e56e80a309d Mon Sep 17 00:00:00 2001 From: Nobuo Kihara Date: Sun, 29 Jan 2017 00:32:23 +0900 Subject: [PATCH 13/80] Docs ja update 0127 [ci skip] (#13463) * docs/guide-ja/intro-yii.md, docs/guide-ja/output-client-scripts.md updated [ci skip] * docs/guide-ja/output-data-providers.md and output-data-widgets.md updated [ci skip] * docs/guide-ja updates (WIP) [ci skip] --- docs/guide-ja/README.md | 16 +-- docs/guide-ja/intro-yii.md | 2 +- docs/guide-ja/output-client-scripts.md | 220 +++++++++++++++++++++++++-------- docs/guide-ja/output-data-providers.md | 2 +- docs/guide-ja/output-data-widgets.md | 40 ++++-- docs/guide-ja/output-formatting.md | 2 +- docs/guide-ja/rest-authentication.md | 2 +- docs/guide-ja/rest-quick-start.md | 7 +- docs/guide-ja/rest-rate-limiting.md | 2 +- docs/guide-ja/rest-resources.md | 37 +++++- docs/guide-ja/rest-routing.md | 2 +- 11 files changed, 251 insertions(+), 81 deletions(-) diff --git a/docs/guide-ja/README.md b/docs/guide-ja/README.md index b9af4c4..ad31716 100644 --- a/docs/guide-ja/README.md +++ b/docs/guide-ja/README.md @@ -178,14 +178,14 @@ RESTful ウェブサービス ウィジェット ------------ -* GridView: **未定** デモページへリンク -* ListView: **未定** デモページへリンク -* DetailView: **未定** デモページへリンク -* ActiveForm: **未定** デモページへリンク -* Pjax: **未定** デモページへリンク -* Menu: **未定** デモページへリンク -* LinkPager: **未定** デモページへリンク -* LinkSorter: **未定** デモページへリンク +* [GridView](http://www.yiiframework.com/doc-2.0/yii-grid-gridview.html) +* [ListView](http://www.yiiframework.com/doc-2.0/yii-widgets-listview.html) +* [DetailView](http://www.yiiframework.com/doc-2.0/yii-widgets-detailview.html) +* [ActiveForm](http://www.yiiframework.com/doc-2.0/guide-input-forms.html#activerecord-based-forms-activeform) +* [Pjax](http://www.yiiframework.com/doc-2.0/yii-widgets-pjax.html) +* [Menu](http://www.yiiframework.com/doc-2.0/yii-widgets-menu.html) +* [LinkPager](http://www.yiiframework.com/doc-2.0/yii-widgets-linkpager.html) +* [LinkSorter](http://www.yiiframework.com/doc-2.0/yii-widgets-linksorter.html) * [Bootstrap ウィジェット](https://github.com/yiisoft/yii2-bootstrap/blob/master/docs/guide-ja/README.md) * [jQuery UI ウィジェット](https://github.com/yiisoft/yii2-jui/blob/master/docs/guide-ja/README.md) diff --git a/docs/guide-ja/intro-yii.md b/docs/guide-ja/intro-yii.md index 8322afc..b56c87f 100644 --- a/docs/guide-ja/intro-yii.md +++ b/docs/guide-ja/intro-yii.md @@ -46,7 +46,7 @@ Yii は現在、利用可能な二つのメジャーバージョン、すなわ 必要条件と前提条件 ------------------ -Yii 2.0 は PHP 5.4.0 以上を必要とします。 +Yii 2.0 は PHP 5.4.0 以上を必要とし、PHP 7 の最新バージョンで最高の力を発揮します。 個々の機能に対する詳細な必要条件は、全ての Yii リリースに含まれている必要条件チェッカを走らせることによって知ることが出来ます。 Yii を使うためには、オブジェクト指向プログラミング (OOP) の基本的な知識が必要です。 diff --git a/docs/guide-ja/output-client-scripts.md b/docs/guide-ja/output-client-scripts.md index f0c71dc..cc8b4cd 100644 --- a/docs/guide-ja/output-client-scripts.md +++ b/docs/guide-ja/output-client-scripts.md @@ -1,98 +1,216 @@ クライアントスクリプトを扱う ============================ -> Note: この節はまだ執筆中です。 +今日のウェブアプリケーションでは、静的な HTML ページがレンダリングされてブラウザに送信されるだけでなく、 +JavaScript によって、既存の要素を操作したり、新しいコンテントを AJAX でロードしたりして、ブラウザに表示されるページを修正します。 +この節では、JavaScript と CSS をウェブサイトに追加したり、それらを動的に調整するために Yii によって提供されているメソッドを説明します。 -### スクリプトを登録する +## スクリプトを登録する -[[yii\web\View]] オブジェクトに対してスクリプトを登録することが出来ます。 +[[yii\web\View]] オブジェクトを扱う際には、フロントエンドスクリプトを動的に登録することが出来ます。 このための専用のメソッドが二つあります。 -すなわち、インラインスクリプトのための [[yii\web\View::registerJs()|registerJs()]] と、外部スクリプトのための [[yii\web\View::registerJsFile()|registerJsFile()]] です。 -インラインスクリプトは、設定のためや、動的に生成されるコードのために有用なものです。 -次のようにして、これらを追加するメソッドを使うことが出来ます。 + +- インラインスクリプトのための [[yii\web\View::registerJs()|registerJs()]] +- 外部スクリプトのための [[yii\web\View::registerJsFile()|registerJsFile()]] +### インラインスクリプトを登録する + +インラインスクリプトは、設定や、動的に生成されるコードのために有用なものです。 +また、[ウィジェット](structure-widgets.md) に含まれる再利用可能なフロントエンドコードによって生成されるコード断片もインラインスクリプトです。 +インラインスクリプトを追加するためのメソッド [[yii\web\View::registerJs()|registerJs()]] は、次のようにして使うことが出来ます。 ```php -$this->registerJs("var options = ".json_encode($options).";", View::POS_END, 'my-options'); +$this->registerJs( + "$('#myButton').on('click', function() { alert('ボタンがクリックされました'); });", + View::POS_READY, + 'my-button-handler' +); ``` -最初の引数は、ページに挿入したい実際の JS コードです。 -二番目の引数は、スクリプトがページのどの場所に挿入されるべきかを決定します。 +最初の引数は、ページに挿入したい実際の JS コードです。これが `