مطالب
یافتن اکشن متد‌های Post ایی در ASP.NET MVC که فیلتر CSRF ندارند
روش مرسوم مقابله با حملات CSRF در ASP.NET MVC، استفاده از فیلتر امنیتی ValidateAntiForgeryToken بر روی اکشن متدهایی از نوع Post است و سپس فراخوانی Html.AntiForgeryToken در View متناظر.
با بالا رفتن تعداد اکشن متدهای یک پروژه، ممکن است استفاده از ValidateAntiForgeryToken فراموش شود. در ادامه مثالی را ملاحظه می‌کنید که یک پروژه‌ی ASP.NET MVC را جهت یافتن اکشن متدهای Post ایی که فیلتر ValidateAntiForgeryToken ندارند، اسکن می‌کند:

using System;
using System.Linq;
using System.Reflection;
// Add a ref. to \Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Web.Mvc.dll
using System.Web.Mvc;
// Add a ref. to System.Web
using System.Web.UI;

namespace FindOutputCaches
{
    class Program
    {
        static void Main(string[] args)
        {
            var path = @"D:\path\bin\site.dll";
            var asmTarget = Assembly.LoadFrom(path);

            checkCsrfTokens(asmTarget);

            Console.WriteLine("Press a key...");
            Console.Read();
        }


        private static void checkCsrfTokens(Assembly asmTarget)
        {
            // یافتن کلیه کنترلرها
            var controllers = asmTarget.GetTypes()
                                       .Where(type => typeof(IController).IsAssignableFrom(type) &&
                                                     !type.Name.StartsWith("T4MVC"))
                                       .ToList();

            foreach (var controller in controllers)
            {
                // یافتن کلیه اکشن متدهای کنترلر جاری
                var actionMethods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                                              .Where(method => typeof(ActionResult).IsAssignableFrom(method.ReturnType))
                                              .ToList();

                foreach (var method in actionMethods)
                {                    
                    var httpPostAttributes = method.GetCustomAttributes(typeof(HttpPostAttribute), true);
                    if (httpPostAttributes == null || !httpPostAttributes.Any())
                        continue;

                    var csrfTokens = method.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), true);
                    if (csrfTokens == null || !csrfTokens.Any())
                    {
                        Console.WriteLine("Detected [HttpPost] without [ValidateAntiForgeryToken] in:\n {0}-->{1}",
                                               controller.FullName, method.Name);
                    }
                }
            }
        }
    }
}
ابتدا مسیر اسمبلی کامپایل شده پروژه ASP.NET MVC که حاوی کنترلرهای برنامه است، باید مشخص گردد.
سپس در این اسمبلی، کلیه نوع‌های تعریف شده، یافت گردیده و آن‌هایی که پیاده سازی کننده IController هستند (یعنی کلاس‌های کنترلر واقعی برنامه)، جدا خواهند شد.
در ادامه در این کنترلرها، متدهایی را بررسی خواهیم کرد که دارای خروجی از نوع ActionResult باشند (فقط اکشن متدها مدنظر هستند). اگر این اکشن متد یافت شده دارای ویژگی HttpPost بود و همچنین ValidateAntiForgeryToken نداشت، یعنی یک مشکل امنیتی که باید برطرف شود.
اشتراک‌ها
پیاده‌سازی فرآیند Impersonation در ASP.NET Core

Impersonation Process

Impersonation is when an admin user is logged in with the same privileges as a user, but without knowing their password or other credentials. I’ve used this in couple applications and it was invaluable for support cases and debugging user permissions. 

[Authorize(Roles = "Admin")] // <-- Make sure only admins can access this 
public async Task<IActionResult> ImpersonateUser(String userId)
{
    var currentUserId = User.GetUserId();

    var impersonatedUser = await _userManager.FindByIdAsync(userId);

    var userPrincipal = await _signInManager.CreateUserPrincipalAsync(impersonatedUser);

    userPrincipal.Identities.First().AddClaim(new Claim("OriginalUserId", currentUserId));
    userPrincipal.Identities.First().AddClaim(new Claim("IsImpersonating", "true"));

    // sign out the current user
    await _signInManager.SignOutAsync();

    await HttpContext.Authentication.SignInAsync(cookieOptions.ApplicationCookieAuthenticationScheme, userPrincipal);

    return RedirectToAction("Index", "Home");
}


پیاده‌سازی فرآیند Impersonation در ASP.NET Core
نظرات مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- تنظیمات IoC Container آن (برنامه‌ای که آزمایش شده و کار می‌کند) در اینجا هست. کدهای خودتان را با آن مقایسه کنید.
- آدرسی که کار می‌کند، یعنی جزو مسیریابی سیستم ثبت شده‌است.
- برای فعال سازی CORS این مراحل باید طی شوند:
در فایل WebApiConfig.cs، در ابتدای متد Register این کدها باید اضافه شوند:
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
config.MessageHandlers.Add(new PreflightRequestsHandler());
در گردش کاری CORS، کلاینت قبل از ارسال درخواست‌های delete، put و post، ابتدا درخواستی از نوع option را ارسال می‌کند تا وضعیت دسترسی را بررسی کند:
public class PreflightRequestsHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Headers.Contains("Origin") && request.Method.Method == "OPTIONS")
        {
            var response = new HttpResponseMessage {StatusCode = HttpStatusCode.OK};
            response.Headers.Add("Access-Control-Allow-Origin", "*");
            response.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization");
            response.Headers.Add("Access-Control-Allow-Methods", "*");
            var tsc = new TaskCompletionSource<HttpResponseMessage>();
            tsc.SetResult(response);
            return tsc.Task;
        }
        return base.SendAsync(request, cancellationToken);
    }
}
به علاوه در ابتدای کدهای سمت کلاینت هم این سطر باید اضافه شود:
jQuery.support.cors = true;
نظرات مطالب
آپلود فایل توسط فرم‌های پویای jqGrid
اینم کد کامل Upload:
    function doUpload(response, postdata, rowId) {
        var result = $.parseJSON(response.responseText);
        if (result.id < 1)
            return [false, "عملیات ثبت موفقیت آمیز نبود", result.id];

        var fileElementId = 'xThumbnail';
        if (rowId) {
            fileElementId = rowId + "_" + fileElementId;
        }

        var val = $("#" + fileElementId).val();
        if (val == '' || val === undefined) {
            return [false, "لطفا فایلی را انتخاب کنید", result.id];
        }

        alert(fileElementId);
        $('#grid1').block({ message: '<h4>در حال ارسال فایل به سرور</h4>' });
        $.ajaxFileUpload({
            url: "@Url.Action("UploadFile", "ProductTypes", new { area = "Admin" })",
            secureuri: false,
            fileElementId: fileElementId, 
            dataType: 'json',
            data: { id: result.id }, 
            complete: function () {
                $('#grid1').unblock();
            },
            success: function (data, status) {
                $("#grid").trigger("reloadGrid");
            },
            error: function (data, status, e) {
                alert(e);
            }
        });

        return [true, "با تشکر!", result.id];
    }
مطالب
یافتن «مطالب مرتبط» توسط لوسین
موتور لوسین علاوه بر فراهم آوردن امکان جستجوی سریع بر روی متون حجیم، امکان یافتن مطالبی مشابه یا مرتبط با مطلبی خاص را نیز فراهم می‌کند. نمونه آن‌را شاید در بعضی از انجمن‌ها یا وبلاگ‌ها دیده باشید که در ذیل مطلب جاری، چندین لینک را به مطالبی مشابه نیز نمایش می‌دهند. در ادامه نحوه استفاده از این قابلیت را در لوسین بررسی خواهیم کرد.  


یافتن شماره سند متناظر لوسین

همان مثال «استفاده از لوسین برای برجسته سازی عبارت جستجو شده در نتایج حاصل» را در نظر بگیرید. در ابتدا نیاز است شماره یک مطلب را تبدیل به شماره سند لوسین کنیم. برای مثال ممکن است Id یک مطلب 1000 باشد، اما شماره سند متناظر آن در لوسین 800 ثبت شده باشد. بنابراین جستجوی ذیل الزامی است:

static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_29;
static readonly IndexSearcher _searcher = new IndexSearcher(@"c:\path\idx", readOnly: true);
private static int GetLuceneDocumentNumber(int postId)
{
    var analyzer = new StandardAnalyzer(_version);
    var parser = new QueryParser(_version, "Id", analyzer);
    var query = parser.Parse(postId.ToString());
    var doc = _searcher.Search(query, 1);
    if (doc.totalHits == 0)
    {
        return 0;
    }
    return doc.scoreDocs[0].doc;
}

در اینجا بر اساس شماره یک مطلب، کوئری متناظر با آن تشکیل شده و جستجویی بر روی اسناد ثبت شده در ایندکس‌های لوسین صورت می‌گیرد. اگر اطلاعاتی یافت شد، شماره سند متناظر بازگشت داده می‌شود.
از این جهت به شماره سند یاد شده نیاز داریم که قرار است مطالب مرتبط با کل این سند را بیابیم.


ساختن کوئری‌های MoreLikeThis

امکانات یافتن مطالب مشابه یک مطلب در اسمبلی Lucene.Net.Contrib.Queries.dll قرار دارد. بنابراین در اینجا نیاز به فایل‌های پروژه Lucene.Net Contrib وجود دارد.
پس از یافتن شماره سند متناظر با یک مطلب، اکنون نوبت به ساخت کوئری‌های پیشرفته MoreLikeThis است که نحوه انجام تنظیمات آن‌را در ذیل مشاهده می‌کنید:

private static Query CreateMoreLikeThisQuery(int postId)
{
    var docNum = GetLuceneDocumentNumber(postId);
    if (docNum == 0)
            return null;

     var analyzer = new StandardAnalyzer(_version);
     var reader = _searcher.GetIndexReader();

     var moreLikeThis = new MoreLikeThis(reader);
     moreLikeThis.SetAnalyzer(analyzer);
     moreLikeThis.SetFieldNames(new[] { "Title", "Body"});
     moreLikeThis.SetMinDocFreq(1);
     moreLikeThis.SetMinTermFreq(1);
     moreLikeThis.SetBoost(true);

     return moreLikeThis.Like(docNum);
}

در اینجا فیلدهایی که قرار است در جستجو حضور داشته باشند توسط متد SetFieldNames معرفی می‌شوند. توسط متد SetMinDocFreq مشخص می‌کنیم که واژه‌های مشابه و مرتبط باید حداقل در چند سند ظاهر شده باشند. همچنین توسط متد SetMinTermFreq تعیین می‌گردد که یک واژه باید چندبار در این اسناد وجود داشته باشد. متد SetBoost سبب می‌شود که آنالیز بهتری بر اساس رتبه بندی‌های حاصل صورت گیرد.


نمایش مطالب مرتبط توسط کوئری MoreLikeThis

پس از این تنظیمات، متد moreLikeThis.Like، یک شیء Query را در اختیار ما قرار خواهد داد. از اینجای کار به بعد همانند سایر مطالب مشابه است. بر اساس این کوئری، جستجویی صورت گرفته و سپس اطلاعات یافت شده نمایش داده می‌شود:

private static void ShowMoreLikeThisPostItems(int postId)
{
     var query = CreateMoreLikeThisQuery(postId);
     if (query == null)
              return;

      var hits = _searcher.Search(query, n: 10);
      foreach (var item in hits.scoreDocs)
      {
           var doc = _searcher.Doc(item.doc);
           var id = doc.Get("Id");
           var title = doc.Get("Title");
           Console.WriteLine(title);
       }
}

مطالب
تهیه پردازنده‌های سفارشی برای افزونه XMLWorker کتابخانه iTextSharp
پیشتر مطلب «تهیه پردازنده‌های سفارشی برای HTMLWorker کتابخانه iTextSharp» را در این سایت مطالعه کرده‌اید. از آنجائیکه افزونه HTMLWorker منسوخ شده است و دیگر پشتیبانی نخواهد شد، باید کدهای فعلی را به افزونه XMLWorker منتقل کرد. مقدمه‌ای را در این زمینه در مطلب «تبدیل HTML فارسی به PDF با استفاده از افزونه‌ی XMLWorker کتابخانه‌ی iTextSharp» می‌توانید مطالعه نمائید.
در ادامه قصد داریم همان امکان پشتیبانی از تصاویر base64 مدفون شده در صفحات HTML را به کتابخانه XMLWorker نیز اضافه کنیم.


تهیه پردازنده‌های سفارشی تگ‌های HTML جهت افزونه XMLWorker

در اینجا کار با ارث بری از کلاس AbstractTagProcessor شروع می‌شود. سپس کافی است تا متد End این کلاس پایه را override کرده و تگ سفارشی خود را پردازش کنیم. نمونه‌هایی از این نوع پردازنده‌ها را در پوشه itextsharp.xmlworker\iTextSharp\tool\xml\html سورس‌های iTextSharp می‌توانید ملاحظه کنید و جهت ایده گرفتن بسیار مناسب می‌باشند.
یکی از این پردازنده‌های پیش فرض موجود در افزونه XMLWorker کار پردازش تگ‌های img را انجام می‌دهد و در کلاس iTextSharp.tool.xml.html.Image قرار گرفته است. این پردازنده به صورت پیش فرض تصاویر base64 را پردازش نمی‌کند. برای رفع این مشکل می‌توان به نحو ذیل عمل کرد:
    public class CustomImageTagProcessor : iTextSharp.tool.xml.html.Image
    {
        public override IList<IElement> End(IWorkerContext ctx, Tag tag, IList<IElement> currentContent)
        {
            IDictionary<string, string> attributes = tag.Attributes;
            string src;
            if (!attributes.TryGetValue(HTML.Attribute.SRC, out src))
                return new List<IElement>(1);

            if (string.IsNullOrEmpty(src))
                return new List<IElement>(1);

            if (src.StartsWith("data:image/", StringComparison.InvariantCultureIgnoreCase))
            {
                // data:[<MIME-type>][;charset=<encoding>][;base64],<data>
                var base64Data = src.Substring(src.IndexOf(",") + 1);
                var imagedata = Convert.FromBase64String(base64Data);
                var image = iTextSharp.text.Image.GetInstance(imagedata);

                var list = new List<IElement>();
                var htmlPipelineContext = GetHtmlPipelineContext(ctx);
                list.Add(GetCssAppliers().Apply(new Chunk((iTextSharp.text.Image)GetCssAppliers().Apply(image, tag, htmlPipelineContext), 0, 0, true), tag, htmlPipelineContext));
                return list;
            }
            else
            {
                return base.End(ctx, tag, currentContent);
            }
        }
    }
با ارث بری از کلاس پردازنده پیش فرض تگ‌های تصاویر یا iTextSharp.tool.xml.html.Image شروع و سپس متد End آن‌را  تحریف کرده‌ایم.
در اینجا اگر src یک تگ img با الگوی تصاویر base64 شروع شده باشد، تصویر آن استخراج و تبدیل به وهله‌ای از تصاویر استاندارد iTextSharp می‌شود. سپس این تصویر در اختیار htmlPipelineContext قرار داده خواهد شد و یا اگر این تصویر از نوع base64 نباشد، همان متد base.End کلاس پایه، جهت پردازش آن کفایت می‌کند.


استفاده از یک پردازنده تگ سفارشی HTML افزونه XMLWorker

تا اینجا یک پردازنده جدید تصاویر را ایجاد کرده‌ایم. برای اینکه XMLWorker را از وجود آن مطلع کنیم، نیاز است آن‌را به درون htmlPipeline این افزونه تزریق نمائیم که کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
            using (var doc = new Document(PageSize.A4))
            {
                var writer = PdfWriter.GetInstance(doc, new FileStream("test.pdf", FileMode.Create));
                doc.Open();
                var html = @"<img src='data:image/png;base64,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' width='62' height='80' style='float: left; margin-right: 28px;' />";

                var tagProcessors = (DefaultTagProcessorFactory)Tags.GetHtmlTagProcessorFactory();
                tagProcessors.RemoveProcessor(HTML.Tag.IMG); // remove the default processor
                tagProcessors.AddProcessor(HTML.Tag.IMG, new CustomImageTagProcessor()); // use our new processor

                var cssResolver = new StyleAttrCSSResolver();
                cssResolver.AddCss(@"code { padding: 2px 4px; }", "utf-8", true);
                var charset = Encoding.UTF8;
                var hpc = new HtmlPipelineContext(new CssAppliersImpl(new XMLWorkerFontProvider()));
                hpc.SetAcceptUnknown(true).AutoBookmark(true).SetTagFactory(tagProcessors); // inject the tagProcessors
                var htmlPipeline = new HtmlPipeline(hpc, new PdfWriterPipeline(doc, writer));
                var pipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
                var worker = new XMLWorker(pipeline, true);
                var xmlParser = new XMLParser(true, worker, charset);
                xmlParser.Parse(new StringReader(html));
            }
            Process.Start("test.pdf");
در اینجا ابتدا لیست پردازنده‌های پیش فرض افزونه XMLWorker را دریافت و سپس پردازنده تگ img آن‌را حذف و با نمونه جدید خود جایگزین کرده‌ایم. در ادامه این لیست تغییر یافته به درون HtmlPipelineContext تزریق شده‌است تا بجای DefaultTagProcessorFactory اصلی مورد استفاده قرار گیرد.
مطالب
استفاده از لنگر (anchor) برای اسکرول به قسمت خاصی از صفحه در Blazor Server
فرض کنید کدی مانند زیر را در یک کامپوننت داریم و انتظار این است که با کلیک بر روی Section2، به بخش مورد نظر اسکرول شویم:
@page "/test"

<nav>
    <!-- یک روش -->
    <a href="#section2">Section2</a>

    <!-- روش دیگر -->
    <NavLink href="#section2">Section2</NavLink>    
</nav>

@* ... *@


<h2 id="section2">It's Section2.</h2>
@* ... *@
اما متاسفانه در Blazor Server تا نسخه فعلی آن (نسخه هفت)، این کار ساده به راحتی امکان‌پذیر نیست. همانطور که ملاحظه می‌کنید، به دو روش، نویگیشن انجام شده‌است؛ اما هیچ‌یک ما را به هدف نمی‌رسانند. دلیل این موضوع، رفتار Blazor Server در بارگذاری صفحات می‌باشد. در حقیقت المان‌ها موقع بارگذاری، هنوز در صفحه وجود ندارند. در واقع ابتدا نیاز است که اتصال SignalR برقرار شود و سپس داده‌ها از سرور دریافت شوند (مگر در حالت pre-rendered که مشکلات خاص خود را در پی دارد).
برای انجام این کار دو روش وجود دارد؛ یکی بر پایه‌ی جاوااسکریپت است و دیگری توسط توابع داخلی Blazor JS.


روش جاوااسکریپتی

ابتدا یک کامپوننت را به نام AnchorNavigation ایجاد می‌نماییم:
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IDisposable
@code {
    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await ScrollToFragment();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }

    private async void OnLocationChanged(object sender, LocationChangedEventArgs e)
    {
        await ScrollToFragment();
    }

    private async Task ScrollToFragment()
    {
        var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
        var fragment = uri.Fragment;
        if (fragment.StartsWith('#'))
        {
            // Handle text fragment (https://example.org/#test:~:text=foo)
            // https://github.com/WICG/scroll-to-text-fragment/
            var elementId = fragment.Substring(1);
            var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
            if (index > 0)
            {
                elementId = elementId.Substring(0, index);
            }

            if (!string.IsNullOrEmpty(elementId))
            {
                await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId);
            }
        }
    }
}
سپس کد جاوا اسکریپتی زیر را در جایی قبل از فراخوانی <script src="_framework/blazor.server.js"></script> قرار می‌دهیم (برای مثال اگر می‌خواهیم در اکثر صفحات از آن بهره ببریم، آن را در layout.cshtmlـ قرار می‌دهیم).
function BlazorScrollToId(id) {
            const element = document.getElementById(id);
            if (element instanceof HTMLElement) {
                element.scrollIntoView({
                    behavior: "smooth",
                    block: "start",
                    inline: "nearest"
                });
            }
        }
حال در هر کامپوننتی که نیاز به استفاده از لنگر (anchor) داریم، به شکل زیر عمل می‌کنیم:
@page "/"

<PageTitle>Index</PageTitle>

<a href="#section2">
    <h1>Section2</h1>
</a>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<div id="section2">
    <h2>It's Section2. </h2>
</div>

<AnchorNavigation />


روش استفاده از توابع داخلی Blazor JS 

می توان از ElementReference و FocusAsync که در حقیقت مربوط به خود Blazor JS می‌باشند استفاده نمود. اینبار کدهای کامپوننت AnchorNavigation را به شکل زیر تغییر می‌دهیم:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using System.Diagnostics.CodeAnalysis;

namespace TestAnchorNavigation;

public class AnchorNavigation: ComponentBase, IDisposable
{
    private bool _setFocus;

    [Inject] private NavigationManager NavManager { get; set; } = default!;
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public string? BookmarkName { get; set; }
    [DisallowNull] public ElementReference? Element { get; private set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "span");
        builder.AddAttribute(2, "tabindex", "-1");
        builder.AddContent(3, this.ChildContent);
        builder.AddElementReferenceCapture(4, this.SetReference);
        builder.CloseElement();
    }

    protected override void OnInitialized()
        => NavManager.LocationChanged += this.OnLocationChanged;

    protected override void OnParametersSet()
        => _setFocus = this.IsMe();

    private void SetReference(ElementReference reference)
        => this.Element = reference;

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        if (this.IsMe())
        {
            _setFocus = true;
            this.StateHasChanged();
        }
    }

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (_setFocus)
            await this.Element!.Value.FocusAsync(false);

        _setFocus = false;
    }

    private bool IsMe()
    {
        string? elementId = null;

        var uri = new Uri(this.NavManager.Uri, UriKind.Absolute);
        if (uri.Fragment.StartsWith('#'))
        {
            elementId = uri.Fragment.Substring(1);
            return elementId == BookmarkName;
        }
        return false;
    }

    public void Dispose()
        => NavManager.LocationChanged -= this.OnLocationChanged;
}
و پیرو آن، صفحه‌ی موردنظر برای استفاده از لنگر نیز به شکل زیر تغییر خواهد کرد:
@page "/"

<PageTitle>Index</PageTitle>

<NavLink href="#section2">
    <h1>Section2</h1>
</NavLink>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<AnchorNavigation BookmarkName="section2">
      <h2>It's Section2. </h2>
</AnchorNavigation>
مطالب
تشخیص نوع فایل با استفاده از محتوای فایل
بی‌شک اگر در سایت خود بخشی را برای دریافت فایل‌های کاربر قرار داده باشید یکی از دغدغه‌های شما اعمال فیلتر و محدودیت روی نوع فایل‌های آپلود شده توسط کاربران خواهد بود. ممکن است سیاست شما پذیرای فایل هایی با پسوند خاص (برای مثال فقط عکس) باشد ولی هیچ تضمینی وجود ندارد که فایلی با پسوند مورد نظر شما محتوایی مشابه با پسوند خود داشته باشد
اگر بخواهیم دقیق‌تر به این موضوع نگاه کنیم فرض می‌کنیم شما در وب سایت خود قسمتی برای آپلود عکس‌های کاربر قرار داده باشید و با مکانیزمی مانند این روش صحت نوع فایل‌های آپلود شده توسط کاربر را بررسی کنید.
اگر شخصی به قصد تخریب و هدر دادن فضای ذخیره سازی شما فایلی با محتوایی غیر از عکس (برای مثال یک فایل اجرایی) را تغییر پسوند داده و به جای عکس آپلود کند چه اتفاقی می‌افتد؟!
دو دلیل مهم برای چک کردن محتوای فایل خواهیم داشت:
1- جلوگیری از اتلاف حافظه ذخیره سازی (ممکن است شخص مهاجم هزاران فایل چند مگابایتی با محتوایی غیر از پسوند فایل بر روی سرور قرار دهد)
2- جلوگیری از اختلال در نمایش فایل‌های آپلود شده (بی شک عدم نمایش عکس در سایت چهره خوبی نخواهد داشت و ممکن است اعتبار سایت را زیر سوال ببرد)

همیشه برای چک کردن نوع فایل باید به دونکته توجه داشت: یکی آنکه پسوند فایل را حتما چک کنیم تا مطابق فیلتر و سیاست مورد انتظار ما باشد ، دوم اینکه به روشی که در ادامه توضیح خواهیم داد، با استفاده از محتوای باینری فایل، MimeType آنرا تشخیص دهیم.
برای اینکار ما از یک تابع API استفاده میکنیم که با استفاده از 255 بایت ابتدایی محتوای فایل، نوع فایل را مشخص می‌کند. این تابع API که FindMimeFromData نام دارد، در فایل urlmon.dll قرار دارد و گویا توسط مرورگر IE برای چک کردن نوع فایل، بر اساس محتوای آن مورد استفاده قرار می‌گیرد. ما نیز از این تابع در دات نت بهره گرفته و با پاس دادن آرایه‌ای از بایتها به آن نوع فایل خود را مشخص می‌کنیم.
کلاسی برای اینکار تهیه شده که در زیر مشاهده میکنید:
using System;
using System.Runtime.InteropServices;
using System.Reflection;

namespace Parsnet.Core
{

    public class MimeTypeDetector
    {
        [DllImport(@"urlmon.dll", CharSet = CharSet.Auto)]
        private extern static System.UInt32 FindMimeFromData(
            System.UInt32 pBC,
            [MarshalAs(UnmanagedType.LPStr)] System.String pwzUrl,
            [MarshalAs(UnmanagedType.LPArray)] byte[] pBuffer,
            System.UInt32 cbSize,
            [MarshalAs(UnmanagedType.LPStr)] System.String pwzMimeProposed,
            System.UInt32 dwMimeFlags,
            out System.UInt32 ppwzMimeOut,
            System.UInt32 dwReserverd
        );

        public string GetMimeType(byte[] content)
        {
            var result = "unknown/unknown";

            try
            {
                byte[] buffer = new byte[256];
                var length = (content.Length > 256) ? 256 : content.Length;
                Array.Copy(content, buffer, length);

                System.UInt32 mimetype;
                FindMimeFromData(0, null, buffer, 256, null, 0, out mimetype, 0);
                System.IntPtr mimeTypePtr = new IntPtr(mimetype);
                result = Marshal.PtrToStringUni(mimeTypePtr);
                Marshal.FreeCoTaskMem(mimeTypePtr);
            }
            catch (Exception ex)
            {
                //Log.WriteError(MethodInfo.GetCurrentMethod(), ex);
            }
            

            return result;
        }

    }
}
تنها نکته قابل توجه در این کد این‌است که در بدنه‌ی متد GetMimeType اگر طول آرایه از 256 بایت بیشتر شود، فقط از 256 بایت ابتدایی آن استفاده میکنیم. در غیر اینصورت کل بایت‌های آرایه، برای چک کردن محتوای فایل به کار گرفته میشوند. برای افزایش کارآیی، بهتر است هنگام فراخوانی این تابع، فقط 256 بایت را به آن ارسال کنید. بخصوص اگر حجم فایلی که باید چک شود زیاد باشد.

نحوه‌ی استفاده از کلاس فوق هم بسیار ساده است. برای مثال هنگام آپلود فایل در MVC می‌توانیم از آن استفاده کنیم:
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Upload(HttpPostedFileBase file)
        {
            //ابتدا با مکانیزم مورد نظر خود پسوند فایل را چک میکنیم اگر پسوند معتبری بود بعد محتوای فایل را چک میکنیم

            var data = new byte[256];
            file.InputStream.Read(data, 0, 256);
            var detector = new Parsnet.Core.MimeTypeDetector();
            var mimeType = detector.GetMimeType(data);
            if (CheckWhiteList(mimeType) == true)
            {
                file.SaveAs("yourpath");
            }
            else
            {
                ModelState.AddModelError("InvalidFileContent", "فایل بارگزاری شده مورد پذیرش نیست.");
            }
            return View();
        }
همیشه از یک مکانیزم دیگر برای اعتبارسنجی پسوند فایل استفاده کنید. برای اینکار می‌توان با تلفیق روش فوق و این روش یک کنترل اعتبارسنجی خوب ایجاد کرد که در مطالب بعدی حتما به آن خواهم پرداخت.
مطالب
آموزش LINQ بخش ششم - عملگرهای پرس و جو قسمت اول
عملگرهای استاندارد پرس و جو

در یک طبقه بندی کلی، عملگرهای پرس و جو بر اساس ورودی و خروجی آنها به سه دسته تقسیم می‌شوند:
1- نتیجه‌ی توالی ورودی، بصورت یک توالی، به خروجی ارسال می‌شود.
2- نتیجه‌ی توالی ورودی، بصورت یک عنصر یکتا و واحد به خروجی ارسال می‌شود.
3- اثری از ورودی در توالی خروجی وجود ندارد (این عملگرها عناصر خودشان را تولید می‌کنند).

دسته‌ی آخر شاید کمی عجیب به نطر برسد. این عملگرها هیچ توالی ورودی را دریافت نمی‌کنند. مثلا می‌توان از طریق این عملگر‌ها، یک توالی از اعداد صحیح را تولید کرد.
تقسیم بندی عملگرهای پرس و جو بر اساس عملکرد به صورت زیر می‌باشد : 
  • محدود کننده (Restriction)
where
  • بازتابی (Projection)
Select,SelectMany 
  • جداکننده (Partitioning)
Take,Skip,TakeWhile,SkipWhile 
  • مرتب سازی (Ordering)
OrderBy,OrderByDescending,ThenBy,ThenByDescending,Reverse 
  • گروه بندی (Grouping)
GroupBy 
  • مجموعه (Set)
Concat,Union,Intersect,Except 
  • تبدیل (Conversion)
ToArray,ToList,ToDictionary,ToLookup,OfType,Cast 
  • عنصر(Element)
First,FirstOrDefault,Last,LastOrDefalt,Single,SingleOrDefault 
  • عنصر در (ElementAt)
ElementAtOrDefault,DefaultIfEmpty 
  • تولید (Generation)
Empty,Range,Report 
  • کمی (Quantifier)
Any,All,Contains,SequenceEqual 
  • مجموعه (Aggregate)
Count,LongCount,Sum,Min,Max,Average,Aggregate 
  • اتصال (Join)
Join,GroupJoin,Zip 

در این مطلب عملگرهای محدود کننده، بازتابی و جداکننده، بررسی خواهند شد. بعد از معرفی هر عملگر، معادل عبارت‌های پرس و جوی آنها نیز معرفی خواهند شد.

عملگرهای محدود کننده (Restriction Operators)
این عملگرها یک توالی ورودی را دریافت و یک توالی محدود شده یا به بیان دیگر فیلتر شده را تولید می‌کنند. عناصر توالی خروجی، عناصری هستند که با فیلتر اعمال شده مطابقت دارند.
Where
این عملگر، عناصری را به خروجی ارسال می‌کند که با گزاره‌ی (Predicate) تعریف شده مطابقت داشته باشند.
نکته : گزاره (Predicate) تابعی است که اگر شرط آن تامین شود، مقدار true و در غیر اینصورت مقدار false را باز می‌گرداند.
مثال : 
 Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Where(x => x.Calories >= 200);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
در کد فوق از عملگر where استفاده شده است. گزاره‌ی (x=>x.Calories>=200) به ازای هر غذایی که کالری آن مساوی یا بزرگتر از 200 باشد، مقدار true را باز می‌گرداند.
خروجی کد بالا:
 Sugar
Butter
عملگر where امضای دیگری دارد که اندیس عنصر ورودی توالی را نیز می‌پذیرد. در مثال قبل، اندیس Sugar برابر 0 و اندیس Butter برابر 4 است. پرس و جوی زیر خروجی مشابه مثال قبل را تولید می‌کند.
 IEnumerable<Ingredient> query = ingredients.Where((ingredient, index) => ingredient.Name == "Sugar" || index == 4);
گزاره نوشته شده در این پرس و جو  از نوع <Func<Ingredient,int,bool خواهد بود و پارامتر int، اندیس عنصر در توالی ورودی می‌باشد.

پیاده سازی توسط عبارت‌های پرس و جو
 در روش عبارت‌های پرس و جو، کلمه‌ی کلیدی where به‌همراه یک عبارت منطقی در پرس و جو ظاهر می‌شود:
 IEnumerable<Ingredient> gueryExpression =
from i in ingredients
where i.Calories >= 200
select i;


عملگرهای بازتاب (Projection Operators)

عملگرهای پرس و جوی بازتابی، یک توالی ورودی را دریافت و با تبدیل عناصر آنها، یک توالی خروجی را تولید می‌کنند.

Select
عملگر پرس و جوی select هر عنصر توالی ورودی را به یک عنصر در توالی خروجی تبدیل می‌کند. تعداد عناصر ورودی و خروجی در این حالت یکسان می‌باشند.
پرس و جوی زیر عناصر توالی ورودی Ingredient را به عناصر رشته‌ای در توالی خروجی بازتاب می‌کند. عبارت Lambda تعریف شده، نحوه‌ی بازتاب عناصر را مشخص می‌کند (هر عنصر ingredient به یک عنصر رشته‌ای بازتاب می‌شود):
 IEnumerable<string> query = ingredients.Select(x => x.Name);
  می‌توان توالی خروجی با عناصر صحیح را نیز تولید کرد:  
 IEnumerable<int> query = ingredients.Select(x => x.Name.Length);

در عملیات بازتاب می‌توان یک شیء جدید را در توالی خروجی ایجاد کرد. در کد زیر عناصر Ingredient به یک عنصر جدید از نوع IngredientNameAndLenght بازتاب شده است.
class IngredientNameAndLength
{
    public string Name { get; set; }
    public int Length { get; set; }
    public override string ToString()
    {
      return Name + " " + Length;
    }
}

IEnumerable<IngredientNameAndLength> query = ingredients.Select(x =>
new IngredientNameAndLength
{
   Name = x.Name,
   Length = x.Name.Length
});
پرس و جوی بالا را می‌توان به شکل نوع‌های بی نام نیز بازنویسی کرد. باید دقت شود که نوع بازگشتی این پرس و جو باید از نوع var باشد.
var query = ingredients.Select(x =>
new
{
   Name = x.Name,
   Length = x.Name.Length
});
خروجی کد بالا به شکل زیر است :
{ Name = Sugar, Length = 5 }
{ Name = Egg, Length = 3 }
{ Name = Milk, Length = 4 }
{ Name = Flour, Length = 5 }
{ Name = Butter, Length = 6 }

پیاده سازی توسط عبارت‌های پرس و جو

کلمه‌ی کلیدی select در عبارت‌های پرس و جو، به شکل زیر استفاده می‌شود:
var query = from i in ingredients
select new
{
    Name=i.Name,
    Length=i.Name.Length
};

SelectMany 
برعکس دستور select که به ازای هر عنصر در توالی ورودی، یک عنصر را در توالی خروجی بازتاب می‌کرد، دستور SelectMany ممکن است تعداد عناصر کمتر و یا بیشتری را در توالی خروجی بازتاب کند (انتخاب مقادیر یک مجموعه از مجموعه‌ی دیگر).
عبارت Lambda نوشته شده در عملگر Select، یک مقدار را باز می‌گرداند. اما عبارت Lambda نوشته شده در عملگر SelectMany، یک توالی فرزند (Child Sequence) را ایجاد می‌کند. توالی فرزند ممکن است حاوی تعداد مختلفی از عناصر به ازای هر عنصر در توالی ورودی باشد.
در مثال زیر عبارت Lambda یک توالی فرزند از کاراکتر‌ها ایجاد می‌کند (یک کاراکتر به ازای هر حرف از هر عنصر توالی ورودی). به‌طور مثال عنصر ورودی Sugar، پس از پردازش توسط  عبارت Lambda، یک توالی فرزند با 5 عنصر 's','u','g','e','r' فراهم می‌کند. هر رشته‌ی در توالی Ingredient می‌تواند تعداد حروف متفاوتی داشته باشد. در نتیجه عبارت Lambda، توالی‌های فرزندی با طول‌های مختلف ایجاد می‌کند.
مثال:
string[] ingredients = {"Sugar","Egg","Milk","Flour","Butter"};
IEnumerable<char> query = ingredients.SelectMany(x => x.ToCharArray());
foreach (var item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا :
 S
u
g
a
r
E
g
g
M
i
l
k
F
l
o
u
r
B
u
t
t
e
r

پیاده سازی توسط عبارت‌های پرس و جو

در روش عبارت‌های پرس و جو یک عبارت (clause) اضافی from برای تولید یک توالی فرزند به کار برده می‌شود. خروجی کد زیر مشابه کد قبلی است:
 string[] ingredients = {"Sugar","Egg","Milk","Flour","Butter"};
IEnumerable<char> query2 = from i in ingredients
from c in i.ToCharArray()
select c;

foreach (var item in query2)
{
   Console.WriteLine(item);
}

عملگرهای جداکننده (Partitioning Operators)
عملگر‌های جداکننده، یک توالی ورودی را دریافت و آنها را از هم جدا می‌کنند.

Take
عملگر Takeیک توالی ورودی را دریافت کرده و تعداد مشخصی از توالی را باز می‌گرداند.
مثال: عملگر Take، سه عضو اول توالی Ingredient را باز می‌گرداند:
 Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Take(3);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
 Sugar
Egg
Milk
همچون سایر عملگر‌های پرس و جو، عملگر Take هم می‌تواند بصورت زنجیروار استفاده شود. در مثال زیر ابتدا عملگر Where برای محدود کردن عناصر با شرطی خاص و سپس عملگر Take برای جدا کردن عناصر حاصل از نتیجه‌ی مرحله قبل مورد استفاده قرار گرفته است:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Where(x=>x.Calories>100).Take(2);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
Sugar
Milk

پیاده سازی توسط عبارت‌های پرس و جو

کلمه‌ی کلیدی (Key word) جایگزینی برای عملگر Take وجود ندارد، ولی می‌توان با ترکیب دو روش نوشتن پرس و جو، خروجی مورد نظر را تولید کرد:
 IEnumerable<Ingredient> query =
(from i in ingredients
  where i.Calories > 100
  select i).Take(2);
TakeWhile
عملگر TakeWhile بر عکس عملگر Take تعداد مشخصی را باز می‌گرداند . این عملگر تا زمانی که گزاره با عناصر مطابقت داشته باشد، اجرا می‌شود و در غیر اینصورت خاتمه پیدا می‌کند.
کد زیر تا زمانی که خصوصیت Calorie توالی ورودی بزرگتر و مساوی 100 باشد، عناصر را جدا می‌کند.
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.TakeWhile(x => x.Calories >= 100);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
 Sugar
Egg
Milk
همانطور که مشاهده می‌کنید، وقتی عملگر TakeWhile به عنصری می‌رسد که گزاره‌ی آن را نقض می‌کند، متوقف می‌شود (در اینجا Flour). در حالی که ممکن است عناصری بعد از Flour وجود داشته باشند که با گزاره‌ی TakeWhile تطابق داشته باشند.

پیاده سازی توسط عبارت‌های پرس و جو
برای این عملگر هم کلمه‌ی کلیدی (Key word) جایگزینی وجود ندارد و با ترکیب دو روش نوشتن پرس و جو نتیجه‌ی دلخواه حاصل می‌شود.
 
Skip
این عملگر تعداد مشخصی از عناصر را از ابتدای توالی نادیده گرفته و باقی عناصر را باز می‌گرداند.
کد زیر سه عضو اول توالی را نادیده گرفته و مابقی را باز می‌گرداند:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.Skip(3);
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا :
 Flour
Butter

پیاده سازی توسط عبارت‌های پرس و جو

برای این عملگر هم کلمه‌ی کلیدی (Key word) جایگزینی وجود ندارد و با ترکیب دو روش نوشتن پرس و جو، نتیجه‌ی دلخواه حاصل می‌شود.
با ترکیب عملگر Take و Skip می‌توان اطلاعات را به‌صورت صفحه بندی به کاربر ارائه کرد. مثال زیر این حالت را نشان می‌دهد.
IEnumerable<Ingredient> firstPage = ingredients.Take(2);
IEnumerable<Ingredient> secondPage = ingredients.Skip(2).Take(2);
IEnumerable<Ingredient> thirdPage = ingredients.Skip(4).Take(2);

Console.WriteLine("First Page : ");
foreach (var ingredient in firstPage)
{
   Console.WriteLine(" - " + ingredient.Name);
}

Console.WriteLine("Second Page : ");
foreach (var ingredient in secondPage)
{
   Console.WriteLine(" - " + ingredient.Name);
}

Console.WriteLine("Third Page : ");
foreach (var ingredient in thirdPage)
{
   Console.WriteLine(" - " + ingredient.Name);
}
خروجی کد بالا :
 First Page :
 - Sugar
 - Egg
Second Page :
 - Milk
 - Flour
Third Page :
 - Butter
SkipWhile
عملگر SkipWhile، مثل عملگر TakeWhile، از یک گزاره برای ارزیابی عناصر توالی استفاده می‌کند. این عملگر تا زمانیکه عناصر توالی، گزاره را نقض نکنند، عناصر را نادیده می‌گیرد.

مثال:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=150},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Butter", Calories=200},
};

IEnumerable<Ingredient> query = ingredients.SkipWhile(x => x.Name != "Milk");
foreach (var ingredient in query)
{
   Console.WriteLine(ingredient.Name);
}
خروجی کد بالا:
 Milk
Flour
Butter
مطالب
بومی سازی تاریخ و اعداد در جاوا اسکریپت در سال 2020
احتمالا تا به امروز در برنامه‌های جاوا اسکریپتی خود از کتابخانه‌های ثالث و یا توابع ویژه‌ای برای نمایش شمسی تاریخ، نمایش فارسی اعداد، افزودن جدا کننده‌ی سه رقمی اعداد (جداکننده‌ی هزارگان)، نمایش تاریخ نسبی مانند 1 روز قبل و ... استفاده کرده‌اید. خبر خوب اینکه موتور جاوا اسکریپتی تمام مرورگرهای جدید (از فایرفاکس 29 و کروم 24 به بعد) به صورت توکار یک چنین تبدیل‌هایی را پشتیبانی می‌کنند و برای مثال برای تبدیل تاریخ میلادی به شمسی و نمایش آن، در بسیاری از موارد نیازی به کتابخانه‌ی حجیم moment.js (و یا سایر روش‌های مرسوم خانگی) نیست.


معرفی API استاندارد بومی سازی JavaScript

Internationalization یا به صورت خلاصه i18n (یعنی یک i به همراه 18 حرف و یک n)، پروسه‌ای که در آن برنامه به نحوی طراحی می‌شود تا خروجی آن قابلیت استفاده‌ی برای انواع و اقسام فرهنگ‌ها را داشته باشد. برای مثال دو متد زیر را در نظر بگیرید:
function formatDate(d)
{
  var month = d.getMonth() + 1;
  var date = d.getDate();
  var year = d.getFullYear();
  return month + "/" + date + "/" + year;
}

function formatMoney(amount)
{
  return "$" + amount.toFixed(2);
}
آیا در همه جای دنیا، تاریخ به صورت ماه، روز و سال نمایش داده می‌شود؛ آن هم به صورت میلادی؟ و یا آیا خروجی فرمت شده‌ی یک مقدار پولی، همیشه با دلار شروع می‌شود و نمایش آن نیز با اعداد انگلیسی است؟
پیشتر جاوا اسکریپت برای مدیریت یک چنین مواردی (i18n-aware formatting) از متد toLocaleString استفاده می‌کرد (و هنوز هم برای پشتیبانی از برنامه‌های قدیمی، از API عمومی آن حذف نشده‌است) و خروجی آن از هر مرورگر و پیاده سازی خاصی، به مرورگر دیگری می‌تواند متفاوت باشد؛ حتی اگر جزئیات دقیقی هم درخواست شود. برای رفع این مشکل، استاندارد ECMAScript Internationalization API ارائه شد تا قابلیت‌های توکار i18n جاوا اسکریپت را بهبود بخشیده و همچنین یک‌دست کند. توسط آن امکان انتخاب یک یا چند منطقه‌ی خاص و سپس فرمت کردن تاریخ، اعداد و یا حتی مرتب سازی واژه‌ها و عبارات با معرفی collations، میسر می‌باشد. در اینجا حتی امکان سفارشی سازی این فرمت‌ها نیز پیش‌بینی شده‌است.


معرفی اینترفیس Intl

i18n API در یک شیء سراسری به نام Intl قابل دسترسی است و تعدادی از سازنده‌های آن Intl.Collator ،Intl.DateTimeFormat و Intl.NumberFormat نام دارند؛ مانند:
const result =  new Intl.NumberFormat("fa").format(123456)
برای کار با این شیء، نیازی به import هیچ ماژول و یا کتابخانه‌ای نیست و جزئی از جاوا اسکریپت استاندارد می‌باشد. به همین جهت کار با آن حجمی را به برنامه‌ی شما اضافه نخواهد کرد.

تمام این سازنده‌ها می‌توانند با یک فرهنگ و یا آرایه‌ا‌‌ی از فرهنگ‌های مدنظر شروع شوند:
const portugueseTime =  new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options);
در مثال اول فرهنگ فارسی و در مثال دوم فرهنگ پرتغالی که در برزیل و پرتغال مورد استفاده‌است، ذکر شده‌اند.
پارامتر اختیاری دوم آن‌ها نیز تنظیماتی است که جهت سفارشی سازی این بومی سازی می‌توان تعریف کرد.


نمایش شمسی تاریخ میلادی توسط i18n API

پس از معرفی i18n API، اکنون می‌خواهیم در طی مثال‌هایی، تمام کتابخانه‌های ثالث تبدیل تاریخ میلادی به شمسی را کنار گذاشته و با استفاده از جاوا اسکریپت استاندارد، این تبدیل را انجام دهیم. پارامتر دوم سازنده‌ی new Intl.DateTimeFormat که تنظیمات آن‌را مشخص می‌کند، می‌تواند به همراه ترکیبی از موارد زیر باشد که مقادیر مجاز برای آن‌ها را نیز مشاهده می‌کنید:
{
  weekday: 'narrow' | 'short' | 'long',
  era: 'narrow' | 'short' | 'long',
  year: 'numeric' | '2-digit',
  month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long',
  day: 'numeric' | '2-digit',
  hour: 'numeric' | '2-digit',
  minute: 'numeric' | '2-digit',
  second: 'numeric' | '2-digit',
  timeZoneName: 'short' | 'long',
// Time zone to express it in
  timeZone: 'Asia/Shanghai',
  // Force 12-hour or 24-hour
  hour12: true | false,
// Rarely-used options
  hourCycle: 'h11' | 'h12' | 'h23' | 'h24',
  formatMatcher: 'basic' | 'best fit'
}
برای نمونه، ذکر Intl.DateTimeFormat بدون هیچ تنظیمی و فقط با تعیین فرهنگ فارسی:
var dateFormat = new Intl.DateTimeFormat("fa");
console.log(dateFormat.format(Date.now()));
خروجی «۱۳۹۸/۱۲/۱» را نمایش می‌دهد.


نمایش تاریخ شمسی با فرمت «۱۳۹۸ اسفند ۱, پنجشنبه»

برای تبدیل تاریخ میلادی به شمسی می‌توان از سازنده‌ی new Intl.DateTimeFormat با فرهنگ fa استفاده کرد. در اینجا ذکر مقدار long برای نام روز هفته، سبب درج نام روز می‌شود. نمایش سال به صورت عددی تنظیم شده‌است، ماه را به صورت بلند و نام کامل نمایش می‌دهد و مقدار روز را به صورت عددی درج می‌کند. این اعداد نیز فارسی هستند:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const faDate = new Intl.DateTimeFormat("fa", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
}).format(date);
console.log(faDate);
که برای نمونه سبب درج خروجی «۱۳۹۸ اسفند ۱, پنجشنبه» در کنسول توسعه دهندگان مرورگر خواهد شد.

اگر فقط نیاز به نمایش «۱ اسفند ۱۳۹۸» بود، می‌توان از تنظیمات زیر که در آن ماه، روز و سال ذکر شده‌اند و در آن، ماه به صورت کامل و بلند نمایش داده می‌شود، استفاده کرد:
const isoString = new Date().toISOString();
const date = new Date(isoString);
console.log(
  new Intl.DateTimeFormat("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  }).format(date)
);

یک نکته: همین خروجی را با متد قدیمی toLocaleDateString نیز می‌توان به دست آورد؛ اما روش توصیه شده برای برنامه‌های جدید، همان استفاده از new Intl است.
console.log(
  new Date().toLocaleDateString("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  })
);

نمایش تاریخ شمسی با فرمت «۹۸/۱۲/۱،‏ ۶:۳۰»

برای اینکار پس از ذکر فرهنگ fa، تمام اجزای تاریخ را به صورت عددی مشخص می‌کنیم و سال را نیز دو رقمی نمایش خواهیم داد:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const fmt = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});
console.log(fmt.format(date));
در این حالت اگر نیاز بود حتما اعداد ماه و روز، دو رقمی باشند، می‌توان تنظیم 2-digit را صریحا ذکر کرد:
const faDateTime = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short"
}).format;
const now = Date.now();
console.log(faDateTime(now));
با خروجی «۹۸/۱۲/۰۱،‏ ۱۲:۵۹ (‎+۳:۳۰ گرینویچ)»

و یا اگر «۱ اسفند ۱۳۹۸،‏ ۰۹:۲۹ (UTC)» مدنظر بود، می‌توان ماه را به long تنظیم کرد و مقدار timeZone را صریحا ذکر نمود (که البته ذکر تنظیمات timeZone اختیاری است):
const faTime = new Intl.DateTimeFormat("fa", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short",
  timeZone: "UTC"
});
console.log(faTime.format(now));

نمایش تاریخ‌های نسبی مانند «1 روز بعد»
برای نمایش تاریخ‌های نسبی، می‌توان از شیء new Intl.RelativeTimeFormat استفاده کرد:
const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit", // other values: "lookup"
  numeric: "always", // other values: "auto"
  style: "long", // other values: "short" or "narrow"
});
console.log(rtf.format(-1, "day"));
console.log(rtf.format(1, "day"));
با خروجی‌های «۱ روز پیش» و «۱ روز بعد»


نمایش اعداد فارسی توسط i18n API

احتمالا برای تبدیل اعداد انگلیسی به فارسی و نمایش آن‌ها، متدهایی را برای replace حروف و اعداد طراحی کرده‌اید. به کمک شیء استاندارد Intl.NumberFormat دیگر نیازی به آن‌ها نخواهید داشت!
خروجی شیء Intl.NumberFormat به همراه ذکر فرهنگ فارسی و هیچ تنظیم اضافه‌تری
console.log(new Intl.NumberFormat("fa").format(123456));
به صورت «۱۲۳٬۴۵۶» است که هم اعداد آن فارسی شده‌اند و هم به همراه جداکننده‌ی هزارگان خودکار است.

اگر می‌خواهید این جداکننده‌ی هزارگان نمایش داده نشود، نیاز است تنظیمات آن‌را به همراه useGrouping: false، به صورت زیر ذکر کرد:
console.log(
   new Intl.NumberFormat("fa", { useGrouping: false }).format(123456)
);

این شیء یک مقدار غیرعددی را
console.log(new Intl.NumberFormat("fa").format("تست"));
به صورت «ناعدد» نمایش می‌دهد.

و یا برای نمایش واحد پولی، می‌توان حالت نمایش را به currency و نوع currency را به IRR که ریال است، تنظیم کرد:
const gasPrice = new Intl.NumberFormat("fa", {
  style: "currency",
  currency: "IRR",
  minimumFractionDigits: 3
});
console.log(gasPrice.format(5.2597));
با این خروجی: «‎ریال ۵٫۲۶۰» که در اینجا امکان تنظیم نمایش تعداد اعشار آن نیز میسر است.

برای نمایش درصد پس از اعداد می‌توان از تنظیم زیر استفاده کرد:
const faPercent = new Intl.NumberFormat("fa", {
  style: "percent",
  minimumFractionDigits: 2
}).format;
console.log(faPercent(0.438));
که خروجی «۴۳٫۸۰٪» را نمایش می‌دهد.

و یا برای نمایش ممیز به همراه تنظیم دقت آن داریم:
const persianDecimal = new Intl.NumberFormat("fa", {
  minimumIntegerDigits: 2,
  maximumFractionDigits: 2
});
console.log(persianDecimal.format(3.1416));
با این خروجی: «۰۳٫۱۴»