Создаём короткие ссылки самостоятельно

Программирование

Tagged Under : ,

Короткие ссылки вещь в последнее время необходимая, в особенности, для сервисов, которые дублируют информацию в твиттер, поэтому может так случиться, что потребуется реализовать свой собственный подобный сервис не используя уже существующие bit.ly, goo.gl и т.п..

Я для себя реализовал простейший генератор коротких ссылок, который создаёт ссылку на основе предыдущей. Генератор, собственно, возвращает ссылку без домена, т.е. после получения ссылки придётся его добавить вручную. Вот такой получился статический класс:

public static class LinkCreator
{
    static string alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    static char last = 'Z';
    static char first = '0';

    public static string GenerateUrl(string url)
    {
                if (string.IsNullOrEmpty(url))
                        return first.ToString();
        string shortUrl = "";
        int index = url.IndexOf(last);
        if (index == -1)
        {
            string prefix = url.Substring(0, url.Length - 1);
            char lastfix = Convert.ToChar(url.Substring(url.Length - 1, 1));
            shortUrl = prefix + alpha[alpha.IndexOf(lastfix) + 1];
        }
        else
        {
            if (index == 0)
            {
                if (url == createDupStr(last, url.Length))
                    shortUrl = createDupStr(first, url.Length + 1);
                else
                {
                    shortUrl = url[0] + GenerateUrl(url.Substring(1));
                }
            }
            else
            {
                string substr = url.Substring(index);
                string dupstr = createDupStr(last, url.Length - index);
                if (index == url.Length - 1 || substr == dupstr)
                    shortUrl = GenerateUrl(url.Substring(0, index)) + createDupStr(first, url.Length - index);
                else
                    shortUrl = url.Substring(0, index + 1) + GenerateUrl(url.Substring(index + 1));
            }
        }
        return shortUrl;
    }

    static string createDupStr(char c, int length)
    {
        StringBuilder r = new StringBuilder();
        for (int i = 1; i <= length; i++)
            r.Append(c);
        return r.ToString();
    }
}

Пример использования:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(LinkCreator.GenerateUrl("0"));
        Console.ReadLine();
    }
}

Ссылки генерятся последовательно, т.е. 0, 1, 2, …., a, b, c, …, aa, ab, ac, ….

Задача с собеседования (SQL): а как бы вы решили данную задачу?

Программирование

Tagged Under : , , ,

На прошедшей неделе ради интереса нам предложили решить задачку, которую задавали на собеседовании в какой-то фирме не так давно. Решил попробовать в перерыве между рабочими задачами, чтобы немного переключиться и отвлечься.

Условие задачи: в таблице могут находится 9 чисел из диапазона от 1 до 10, в рандомном порядке. Каждое число встречается один раз. Необходимо написать запрос, который вернёт число из диапазона [1,10] отсутствующее в таблице. Использовать спецфункции нельзя, решение должно быть наиболее простым и понятным. Решение должно быть на SQL.

Я предложил следующее решение: показать решение

А как вы бы решили данную задачу?

OAuth + ASP.NET (Часть 5): авторизация через Odnoklassniki по протоколу OAuth 2.0

Программирование, Проекты

Tagged Under : , , , , , ,

Итак, на очереди авторизация через одноклассники: я не поленился и всё-таки зарегистрировал тестовое приложение. Вопреки ожиданиям всё оказалось гораздо проще.
Итак, метод контроллера, перебрасывающий пользователя на одноклассники для авторизации и для получения кода (используется разработанная ранее библиотека для авторизации через OAuth 1.0 и 2.0 AOAuthNET):

public ActionResult odnoklassniki()
{
    OAuth2 odnoklassniki = new OAuth2("id вашего приложения", "секретный ключ приложения", "http://www.odnoklassniki.ru/oauth/authorize", "http://api.odnoklassniki.ru/oauth/token.do", "http://twitter.kosfiz.net/auth/odnoklassniki/");
    odnoklassniki.GetAuthCode(new Dictionary<string, string>() { { "scope", "" } });
    return View();
}

Метод контроллера, получающий токен и после данные о пользователе:

public ActionResult odnoklassniki(string code)
{
    if (!string.IsNullOrEmpty(code))
    {
        OAuth2 odnoklassniki = new OAuth2("id вашего приложения", "секретный ключ приложения", "http://www.odnoklassniki.ru/oauth/authorize", "http://api.odnoklassniki.ru/oauth/token.do", "http://twitter.kosfiz.net/auth/odnoklassniki/");
        odnoklassniki.Code = code;
        OAuth2Token token = odnoklassniki.GetAccessToken(new Dictionary<string, string> { { "client_secret", "секретный ключ приложения" } }, OAuth2.AccessTokenType.JsonDictionary);
        if (token != null)
        {
            if (token.dictionary_token != null)
            {
                Dictionary<string, string> dict = new Dictionary<string, string>();
                dict.Add("client_id", "id приложения");
                dict.Add("application_key", "публичный ключ");
                dict.Add("method", "users.getCurrentUser");
                Response.Write(OAuth2UserData.GetOdnoklassnikiUserData(token.dictionary_token["access_token"], "секретный ключ", dict));
            }
        }
    }
    return View();
}

Для получения данных о пользователе пришлось доработать класс OAuth2UserData и добавить следующие методы:

public static string GetOdnoklassnikiUserData(string access_token, string secret_key, Dictionary<string,string> dict)
{
    string sig = GetSig(GetMD5(access_token + secret_key), dict);
    dict.Add("access_token", access_token);
    dict.Add("sig", sig);
    return GetUserData(ODNOKLASSNIKI_ME_URL, dict);
}

private static string GetMD5(string key)
{
    MD5 md5 = MD5.Create();
    byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(key));
    return string.Concat(hash.Select(x => x.ToString("x2")).ToList());
}

Пример авторизации через одноклассники с помощью библиотеки AOAuthNET

Исходный код библиотеки AOAuthNET

OAuth + ASP.NET (Часть 4): авторизация через Windows Live ID по протоколу OAuth 2.0

Программирование, Проекты

Tagged Under : , , , , , ,

В рамках проверки своей библиотеки и её расширения решил заняться интеграцией с Windows Live ID. Совсем недавно live не поддерживал OAuth, но время течёт и всё меняется. Теперь если вы рассчитываете привлечь на сайт аудиторию в том числе и c live-аккаунтами не затрудняя их очередной регистрацией, то возможно использовать OAuth для авторизации пользователя на своём сайте и получения данных о нём.

Ниже я приведу пример как это сделать с помощью библиотеки AOAuthNET для ASP.NET MVC сайта, для классического ASP.NET отличий будет немного.

Итак, метод контроллера, отрабатывающий по нажатию пользователя на кнопке войти

[HttpGet]
public ActionResult live()
{
    OAuth2 live = new OAuth2("id приложения", "секретный ключ", "https://oauth.live.com/authorize", "https://oauth.live.com/token", "на эту страницу будет совершён редирект с передачей кода");
    live.GetAuthCode(new Dictionary<string, string>() { { "display", "page" }, { "scope", "wl.basic" } });
    return View();
}

И теперь, собственно, код отвечающий за получения токена на странице, на которую перекинет пользователя с кодом для получения этого самого токена.

public ActionResult live(string code)
{
    if (!string.IsNullOrEmpty(code))
    {
        OAuth2 live = new OAuth2("id приложения", "секретный ключ", "https://oauth.live.com/authorize", "https://oauth.live.com/token", "на эту страницу будет совершён редирект с передачей кода");
        live.Code = code;
        OAuth2Token token = live.GetAccessToken(new Dictionary<string, string> { { "client_secret", "секретный ключ" }}, OAuth2.AccessTokenType.JsonDictionary);
        if (token != null)
        {
            if (token.dictionary_token != null)
            {
                Response.Write(OAuth2UserData.GetLiveUserData(token.dictionary_token["access_token"]));
            }
        }
    }
    return View();
}

Соответственно, я добавил метод GetLiveUserData, который получает информацию о пользователе (имя, фамилия и т.д.):

public static string GetLiveUserData(string access_token)
{
    return GetUserData(LIVE_ME_URL, new Dictionary<string, string>() { {LIVE_ACCESS_TOKEN, access_token} });
}

Всё просто, о методе GetUserData я писал ранее в первых частях цикла.

Пример работы библиотеки AOAuthNET с windows live id

Библиотека AOAuthNET и исходники

Предложения по расширению возможностей и функционалу приветствуются, так что пишите.

OAuth + ASP.NET (Часть 3): авторизация через vkontakte по протоколу OAuth 2.0

Программирование

Tagged Under : , , ,

Буквально вчера узнал, что вконтакте начал открытое тестирование авторизации по протоколу OAuth 2.0 тем самым открыв лёгкий доступ к авторизации через серверный код. Вспомнив, что некогда я писал библиотеку AOAuth.NET для авторизации посредством протоколов OAuth 1.0 и 2.0 я решил интегрировать авторизацию вконтакте в тестовый сайт с помощью данной бибилотеки и при необходимости добавить в неё нужный функционал.

Во-первых, в классе Oauth2 пришлось добавить перегрузки метода GetAuthCode, чтобы он мог принимать дополнительные набор параметров, понадобилось это из-за обязательности параметра display в первом запросе к странице http://api.vkontakte.ru/oauth/authorize для получения кода.

public void GetAuthCode(Dictionary<string, string> additional)
{
    GetAuthCode(_AuthUrl, additional);
}

public void GetAuthCode(string auth_url, Dictionary<string,string> additional)
{
    Dictionary<string, string> parameters = new Dictionary<string, string>();
    parameters.Add(OAUTH_CLIENT_ID, _ClientId);
    parameters.Add(OAUTH_RESPONSE_TYPE, OAUTH_RESPONSE_TYPE_CODE);
    if (!string.IsNullOrEmpty(_RedirectURI))
        parameters.Add(OAUTH_REDIRECT_URI, _RedirectURI);

    if (additional != null)
        foreach (var item in additional)
            parameters.Add(item.Key, item.Value);

    string url = string.Format("{0}{1}", auth_url, OAuthCommonUtils.getParametersString(parameters));
    OAuthCommonUtils.Redirect(url);
}

Собственно, метод контроллера у меня получился аналогичным представленным в предыдущей статье:

[HttpGet]
public ActionResult VK()
{
    OAuth2 vk = new OAuth2("app_id", "secret", "http://api.vkontakte.ru/oauth/authorize", "https://api.vkontakte.ru/oauth/access_token", "http://twitter.kosfiz.net/auth/vk/");
    vk.GetAuthCode(new Dictionary<string, string>() { {"display", "popup"} });
    return View();
}

app_id – id вашего приложения (сайта) в вконтакте,
secret – секретный ключ для вашего приложения можно как и id найти на странице редактирования приложения.

Далее в контроллере принимающем код с вконтакта пишем следующий метод:

public ActionResult vk(string code)
{
    if (!string.IsNullOrEmpty(code))
    {
        OAuth2 vk = new OAuth2("app_id", "secret", "http://api.vkontakte.ru/oauth/authorize", "https://api.vkontakte.ru/oauth/access_token", "http://twitter.kosfiz.net/auth/vk/");
        vk.Code = code;
        OAuth2Token token = vk.GetAccessToken(new Dictionary<string, string> { { "client_secret", "secret" } }, OAuth2.AccessTokenType.JsonDictionary);
        if (token != null)
        {
            if (token.dictionary_token!=null)
            {
            Response.Write(OAuth2UserData.GetVKUserData(token.dictionary_token["access_token"], new Dictionary<string, string> { { "uid", token.dictionary_token["user_id"] } }));
            }
        }
    }
    return View();
}

По сравнению опять же с прошлой статьёй изменился тип парсинга токена, появился OAuth2.AccessTokenType.JsonDictionary и, соответственно, изменился метод GetAccessToken

public OAuth2Token GetAccessToken(string code, Dictionary<string,string> additional, AccessTokenType att)
{
    Dictionary<string, string> parameters = new Dictionary<string, string>();
    parameters.Add(OAUTH_GRANT_TYPE, OAUTH_GRANT_TYPE_VALUE);
    parameters.Add(OAUTH_CODE, code);
    parameters.Add(OAUTH_CLIENT_ID, _ClientId);
    if (!string.IsNullOrEmpty(_RedirectURI))
        parameters.Add(OAUTH_REDIRECT_URI, _RedirectURI);
    if (additional != null)
        foreach (var item in additional)
            parameters.Add(item.Key, item.Value);

    string raw_token = OAuthCommonUtils.sendPostRequest(_TokenUrl, OAuthCommonUtils.getParametersString(parameters).Remove(0,1));
    OAuth2Token token = null;
    switch(att)
    {
        case AccessTokenType.JsonDictionary:
            token = new OAuth2Token();
            JavaScriptSerializer s = new JavaScriptSerializer();
            token.dictionary_token = s.Deserialize<Dictionary<string,string>>(raw_token);
            break;
        case AccessTokenType.Raw:
            token = new OAuth2Token();
            token.raw_token = raw_token;
            break;
        case AccessTokenType.Dictionary:
            token = new OAuth2Token();
            string[] p = raw_token.Split('&');
            foreach (string item in p)
            {
                string[] key_value = item.Split('=');
                token.dictionary_token.Add(key_value[0], key_value[1]);
            }
            break;
        case AccessTokenType.OAuth2Token:
            try
            {
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                token = serializer.Deserialize<OAuth2Token>(raw_token);
                token.raw_token = raw_token;
            }
            catch (Exception exc)
            {
            }
            break;
    }
   
    return token;
}

public enum AccessTokenType
{
    Raw,
    Dictionary,
    JsonDictionary,
    OAuth2Token
}

Ну, и в концовке я добавил следующий метод GetVKUserData

const string VK_ME_URL = "https://api.vkontakte.ru/method/getProfiles";
public static string GetVKUserData(string access_token, Dictionary<string, string> parameters)
{
        parameters.Add("access_token", access_token);
    return GetUserData(VK_ME_URL, parameters);
}

Этого метода достаточно, чтобы имея access_token получить имя и фамилию авторизовавшегося пользователя.

Попробовать можно тут – окно браузера может уменьшаться, я тестировал display = popup
Отсюда можно скачать исходники AOAuth.NET

Orchard CMS: добавляем атрибут title к ссылкам

Программирование

Tagged Under :

К сожалению, метод ItemDisplayLink не добавляет к создаваемой ссылке атрибут title, поэтому я решил это исправить, тем более, что исходный код Orchard под рукой.
Идём в проект Orchard.Framework папка MvcHtml и правим метод ItemDisplayLink в файле ContentItemExtensions.cs следующим образом:

public static MvcHtmlString ItemDisplayLink(this HtmlHelper html, string linkText, IContent content) {
    var metadata = content.ContentItem.ContentManager.GetItemMetadata(content);

            if (metadata == null) return null;
            if (metadata.DisplayRouteValues == null)
                return null;

            string titleText = NonNullOrEmpty(metadata.DisplayText, linkText, "view");
            return html.ActionLink(
                NonNullOrEmpty(linkText, metadata.DisplayText, "view"),
                Convert.ToString(metadata.DisplayRouteValues["action"]),
                metadata.DisplayRouteValues, new Dictionary<string, object>() { { "title", titleText.Replace(""", "'") } });
}

Далее собираем и выкладываем на сайт.
Теперь атрибут title добавлен к ссылкам, но не ко всем: нет этого атрибута у тегов. Следуем в папку Modules/Orchard.Tags/Views/Parts сайта и правим файл Tags.ShowTags.cshtml следующим образом:

@{
    var tagsHtml = new List<IHtmlString>();
    foreach(var t in Model.Tags) {
        if (tagsHtml.Any()) {
            tagsHtml.Add(new HtmlString(", "));
        }
        tagsHtml.Add(Html.ActionLink((string)t.TagName, "Search", "Home", new { area = "Orchard.Tags", tagName = (string)t.TagName }, new { title = (string)t.TagName }));
    }
}

@if (tagsHtml.Any()) {
    <p class="tags">
        <span>@T("Tags:")</span>
        @foreach(var htmlString in tagsHtml) { @htmlString }
    </p>
}

Ну, и на последок облако тегов Vandelay.TagCloud. В папке /Modules/Vandelay.TagCloud/Views/Parts правим TagCloud.cshtml

@using Vandelay.TagCloud.Models;
<ul class="tagcloud">
@foreach (TagCount tagCount in Model.TagCounts) {
    <li class="tagCloud-tag tagCloud-@tagCount.Bucket">
        @Html.ActionLink(tagCount.TagName, "Search", "Home", new {
                area = "Orchard.Tags",
                tagName = tagCount.TagName
            }, new { title = tagCount.TagName}
        )
    </li>
}
</ul>

Спросите зачем всё это? Ну, некоторые сеошники говорят, что это важно )))

Orchard CMS: сообщаем поисковым системам о добавлении новых статей

Программирование

Tagged Under : , , , , ,

В этот раз я попытаюсь рассмотреть процесс создания модуля для Orchard CMS, который будет пинговать поисковые системы (google, yandex, yahoo, bing, ask) сообщая им о том, что обновилась информация на сайте и нужно проверить файл sitemap.xml.

Функционирование модуля можно описать кратко следующим образом: создадим ContentPart (PingContentPart), которую нужно будет присоединять к типам контента (ContentType), при добавлении которых нужно пинговать сервисы; для пинга придётся реализовать небольшой класс; поисковые сервисы (адреса которые нужно пинговать) будут жёстко прописаны в коде.

Прежде всего начнём с создания заготовки модуля:

orchard.exe codegen module SitemapPing

Затем тут же добавляем Migration.cs

orchard.exe codegen datamigration SitemapPing

Открываем проект модуля и изменяем Migration.cs следующим образом:

public int Create() {

    SchemaBuilder.CreateTable("SitemapPingRecord", table => table.ContentPartRecord());
        SchemaBuilder.CreateTable("SitemapPingSettingsRecord", table => table.Column("Id", DbType.Int32, column => column.PrimaryKey().Identity())
                .Column("LastUpdateTime", DbType.DateTime, column => column.Nullable()));

    ContentDefinitionManager.AlterPartDefinition("SitemapPingPart", cfg => cfg.Attachable());

    return 1;
}

В папке Models создаём два класса пустышки: SitemapPingRecord

public class SitemapPingRecord: ContentPartRecord
{
}

SitemapPingPart

public class SitemapPingPart: ContentPart<SitemapPingRecord>
{
}

И служебный класс (в нём будем хранить время последнего пинга к сервисам, чтобы не пинговать чаще чем раз в час)

public class SitemapPingSettingsRecord
{
    public virtual int Id { get; set; }
    public virtual DateTime LastUpdateTime { get; set; }
}

Добавляем папку Services и добавляем интерфейс и класс, соответственно, IPingService и PingService.

public interface IPingService: IDependency
{
    void Ping();
}
public class PingService : IPingService
{
    const int pingTimeout = 5000;

    const string GooglePingUrl = "http://www.google.com/webmasters/sitemaps/ping?sitemap={0}";
    const string YandexPingUrl = "http://ping.blogs.yandex.ru/ping?sitemap={0}";
    const string YahooPingUrl = "http://search.yahooapis.com/SiteExplorerService/V1/ping?sitemap={0}";
    const string BingPingUrl = "http://www.bing.com/webmaster/ping.aspx?siteMap={0}";
    const string AskPingUrl = "http://submissions.ask.com/ping?sitemap={0}";

    IContentManager _contentManager;
    IRepository<SitemapPingSettingsRecord> _repository;

    public PingService(IContentManager contentManager, IRepository<SitemapPingSettingsRecord> repository)
    {
        _contentManager = contentManager;
        _repository = repository;
    }

    public void Ping()
    {
        string siteSitemapUrl = string.Format("http://{0}/sitemap.xml", HttpContext.Current.Request.Url.Host);

        bool IsNeedUpdate = false;
        var settings = _repository.Table.FirstOrDefault();

        if (settings == null)
        {
            IsNeedUpdate = true;
            _repository.Create(new SitemapPingSettingsRecord { LastUpdateTime = DateTime.Now });
        }
        else
            if (Math.Abs(settings.LastUpdateTime.Subtract(DateTime.Now).TotalHours) >= 1)
            {
                IsNeedUpdate = true;
                settings.LastUpdateTime = DateTime.Now;
            }

        if (IsNeedUpdate)
        {
            SendPing(string.Format(GooglePingUrl, siteSitemapUrl));
            SendPing(string.Format(YahooPingUrl, siteSitemapUrl));
            SendPing(string.Format(YandexPingUrl, siteSitemapUrl));
            SendPing(string.Format(BingPingUrl, siteSitemapUrl));
            SendPing(string.Format(AskPingUrl, siteSitemapUrl));
        }

    }

    private void SendPing(string pingServiceUrl)
    {
        HttpWebRequest pingRequest = (HttpWebRequest)WebRequest.Create(pingServiceUrl);
        pingRequest.Method = "GET";
        pingRequest.Timeout = pingTimeout;

        using (HttpWebResponse pingResponse = (HttpWebResponse)pingRequest.GetResponse())
        {
        }
    }
}

Добавляем хендлер

public class SitemapPingHandler: ContentHandler
{
    IPingService _pingService;
    public SitemapPingHandler(IRepository<SitemapPingRecord> repository, IPingService pingService)
    {
        _pingService = pingService;
        Filters.Add(StorageFilter.For(repository));

        OnPublished<SitemapPingPart>((context, part) => { // когда нажали кнопку Publish и опубликовали версию
           
            if ((context.PreviousItemVersionRecord == null && context.PublishingItemVersionRecord.Published) ||
                (context.PreviousItemVersionRecord.Published == false && context.PublishingItemVersionRecord.Published))
            {
                                // сначала, когда заполняем Заголовок (Title) RoutePart идёт ajax-запрос, при том, что ничего не публикуется всё равно отрабатывает OnPublished, поэтому вставляем нижележащие условия, чтобы выделить "настоящий" момент публикации
                var route = context.ContentItem.As<RoutePart>();
                if(route!=null)
                    if (!string.IsNullOrEmpty(route.Slug.Trim()))
                    {
                        _pingService.Ping();
                    }
            }
        });
    }
}

И, наконец, добавляем driver:

public class SitemapPingDriver: ContentPartDriver<SitemapPingPart>
{        
}

Создаём пакет

orchard.exe package create SitemapPing f:

Вот и всё, теперь при публикации записей с ContentType, к которым присоединена SitemapPingPart, будет происходить пинг поисковых сервисов.

Исходники могут немного отличаться от приведённого кода.
Пакет на сайте Orchard

Orchard CMS: фиксим отображение списков

Программирование

Tagged Under :

Хоть авторы и не признают, что это недоработка, но то, что в случае отображения списка не отображается Title в тегах и на странице, а также другие части (ContentPart), например, Meta весьма плохо и неудобно.
Вариант фикса следующий.

Скачиваем последнюю версию, добавляем изменения, собираем и заменяем Orchard.Core.dll на вновь полученную с фиксом, радуемся.

Orchard CMS: делаем модуль для кнопки «Поделиться» от Yandex’а

Программирование

Tagged Under : ,

В очередной раз понадобилось создать модуль, на этот раз это кнопка «Поделиться» от сервиса яндекса Ya.Share.
В идеале хотелось бы, чтобы через панель управления можно было управлять отображаемыми сервисами, видом кнопки и языком. Дополнительно включать и отключать кнопку. Так как показываться кнопка должна только для определённых типов контента, то нужна она, соответственно, как часть контента (ContentPart) причём предусмотреть надо и тот вариант, что кнопок может быть несколько на странице, поэтому js от яндекса не должен дублироваться, а в кнопке принудительно должна указываться ссылка на запись и заголовок (title).
Шаги для создания модуля всё те же:

  • Запускаем командную строку, переходим в папку сайта, на базе которого будет создавать модуль и пишем:
    orchard.exe codegen module YandexShare
  • Добавляем migration.cs, в котором определим в дальнейшем какие таблицы нам понадобиться для хранения настроек:

    orchard.exe codegen datamigration YandexShare
  • Открываем проект созданного модуля в студии и начинаем добавлять необходимый функционал

Прежде всего в папку Models добавляем классы: YandexShareSettingsRecord (настройки модуля), YandexShareRecord (класс пустышка), YandexSharePart (собственно, класс, экземпляр которого будет определять параметры кнопки).

public class YandexShareSettingsRecord
{
    public virtual int Id { get; set; }
    public virtual bool IsEnabled { get; set; }
    public virtual string ShareType { get; set; }
    public virtual string ShareLang { get; set; }
    public virtual string ShareServices { get; set; }
}

public class YandexShareRecord: ContentPartRecord
{
}

public class YandexSharePart: ContentPart<YandexShareRecord>
{
    public string Title { get; set; } // заголовок, который будет отправлен в сервис
    public string Link { get; set; } // ссылка на целевую страницу
    public string ShareServices { get; set; } // отображаемые сервисы
    public string ShareLang { get; set; } // язык для отображения кнопки
    public string ShareType { get; set; } // тип отображения кнопки
}

Раз классы определены, то можно их теперь отразить в базу, поэтому правим Migration.cs следюущим образом:

public int Create() {
    SchemaBuilder.CreateTable("YandexShareRecord", table => table.ContentPartRecord());

    SchemaBuilder.CreateTable("YandexShareSettingsRecord", table => table.Column("Id", DbType.Int32, column => column.Identity().PrimaryKey())
        .Column("ShareType", DbType.String, column => column.NotNull().WithDefault("button"))
        .Column("ShareLang", DbType.String, column => column.NotNull().WithDefault("ru"))
        .Column("ShareServices", DbType.String, column => column.NotNull().WithDefault(""))
        .Column("IsEnabled", DbType.Boolean, column=>column.NotNull().WithDefault(false)));

    ContentDefinitionManager.AlterPartDefinition("YandexSharePart", cfg => cfg.Attachable());

    return 1;
}

Теперь сделаем пункт меню и контроллер с вью, чтобы настраивать кнопку «Поделиться». Для этого добавим к проекту файл AdminMenu.cs следующего содержания:

public class AdminMenu: INavigationProvider
{
    Localizer T { get; set; }

    public AdminMenu()
    {
        T = NullLocalizer.Instance;
    }

    public void GetNavigation(NavigationBuilder builder)
    {
        builder.Add(T("YandexShare"), "50", menu=>menu.Add(T("YandexShare"), "0", item=>item.Action("Index", "Admin", new { area = "YandexShare"}).Permission(StandardPermissions.SiteOwner)));
    }

    public string MenuName
    {
        get { return "admin"; }
    }
}

В нём мы определили в каком меню будет показываться файл (в меню панели управления), отображаться он будет в 50 позиции, называться YandexShare и в качестве обработчика ссылки мы выставили наш контроллер Admin и его действие Index. Не забываем указывать area = имени модуля.

Добавим, кстати, контроллер

[ValidateInput(false), Admin]
public class AdminController:Controller
{
    IYandexShareService _yandexShareService;
    public AdminController(IYandexShareService yandexShareService)
    {
        _yandexShareService = yandexShareService;
    }

    [HttpGet]
    public ActionResult Index()
    {
        YandexShareSettingsViewModel viewModel = new YandexShareSettingsViewModel();
        viewModel.Settings = _yandexShareService.Get();
        viewModel.ShareServicesList = new List<string>("yaru,vkontakte,facebook,twitter,odnoklassniki,moimir,lj,friendfeed,moikrug,blogger,digg,evernote,delicious,gbuzz,greader,juick,liveinternet,linkedin,myspace,yazakladki".Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
       
        Dictionary<string, string> langDict = new Dictionary<string, string> { { "be", "" }, { "en", "" }, { "kk", "" }, { "ru", "" }, { "tt", "" }, { "uk", "" }};
        SelectList langList = new SelectList(langDict, "Key", "Key", viewModel.Settings.ShareLang);
        viewModel.ShareLangList = langList;

        List<string> typeList = new List<string> {"button", "link", "icon", "none" };
        viewModel.ShareTypeList = new SelectList(typeList, viewModel.Settings.ShareType);

        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(bool? IsEnabled, string ShareLang, string ShareType, FormCollection formCollection)
    {
        _yandexShareService.Set(IsEnabled.HasValue ? IsEnabled.Value : false, ShareLang, ShareType, formCollection["ShareServices"]);
        return RedirectToAction("Index");
    }
}

Итак, в конструктор контроллера передаётся экземпляр класс унаследованного от интерфейса IYandexShareService, этот интерфейс и производный класс будут описаны чуть ниже. Сейчас же нужно знать только то, что унаследованный класс позволяет работать с данными о параметрах кнопки.

В Action по GET-запросу создаётся экземпляр класса YandexShareSettingsViewModel

public class YandexShareSettingsViewModel
{
    public YandexShareSettingsRecord Settings { get; set; } // настройки в том виде, в котором они есть
    public SelectList ShareLangList { get; set; } // список поддерживаемых языков
    public SelectList ShareTypeList { get; set; } // возможные типы отображения кнопки
    public List<string> ShareServicesList { get; set; } // список доступных сервисов
}

Данный класс необходим лично мне для упрощения передачи данных во вью и их дальнейшего отображения.
В Action для POST-запроса просто напросто сохраняем данные полученные с формы на странице редактирования.

Кстати, вью для настроек:

@model YandexShare.ViewModels.YandexShareSettingsViewModel

<h1>@Html.TitleForPage(@T("Manage Ya.Share settings").ToString())</h1>
@using (Html.BeginFormAntiForgeryPost())
{
    <fieldset>
    <p>
    <label for="">Enabled</label>
    <input type="checkbox" id="IsEnabled" name="IsEnabled" value="true" @if(Model.Settings.IsEnabled){<text>checked="checked"</text>} />
    </p>
    <p>
    <label for="">Block type:</label>
    @Html.DropDownList("ShareType", Model.ShareTypeList)
    </p>
    <p>
    <label for="">Block lang:</label>
    @Html.DropDownList("ShareLang", Model.ShareLangList);
    </p>
    <p>
    <label for="">Choose services:</label>
    @foreach (string item in Model.ShareServicesList)
    {
        <input type="checkbox" id="ShareServices" name="ShareServices" value="@item" title="@item" @if (Model.Settings.ShareServices.Contains(item))
                                                                                                                       {<text>checked="checked"</text>} /><span>@item</span><br />
    }
    </p>
    <input type="submit" value="Save settings" />
    </fieldset>
}

Теперь стоит всё же рассказать о IYandexShareService. На самом деле это всего лишь интерфейс, который обязывает «наследника» определить методы для получения и обновления настроек нашего модуля. Он чрезвычайно прост:

public interface IYandexShareService: IDependency
{
    YandexShareSettingsRecord Get();
    void Set(bool IsEnabled, string ShareLang, string ShareType, string ShareServices);
}

А наследующий данный интерфейс класс выглядит следующим образом:

public class YandexShareService: IYandexShareService
{
    IRepository<YandexShareSettingsRecord> _repository;
    ICacheManager _cacheManager;
    ISignals _signals;

    public YandexShareService(IRepository<YandexShareSettingsRecord> repository, ICacheManager cacheManager, ISignals signals)
    {
        _repository = repository;
        _signals = signals;
        _cacheManager = cacheManager;
    }

    public YandexShareSettingsRecord Get()
    {
        var settings = _cacheManager.Get("YandexShareSettings", x=>{x.Monitor(_signals.When("YandexShareSettingsChanged")); return _repository.Table.FirstOrDefault();}); // получаем настройки с использованием кеширования
        if(settings==null) // если настроек пока не было произведено, создаём запись с настройками по умолчанию
        {
            _repository.Create(new YandexShareSettingsRecord{ IsEnabled = false, ShareLang = "ru", ShareType = "button", ShareServices = ""});
            settings = _repository.Table.FirstOrDefault();
            _signals.Trigger("YandexShareSettingsChanged");
        }
        return settings;
    }

    public void Set(bool IsEnabled, string ShareLang, string ShareType, string ShareServices)
    {
        var settings = Get();
        if (settings != null) // если есть настройки, то обновляем и сбрасываем кеш
        {
            settings.IsEnabled = IsEnabled;
            settings.ShareLang = ShareLang;
            settings.ShareServices = ShareServices;
            settings.ShareType = ShareType;
            _repository.Update(settings);
            _signals.Trigger("YandexShareSettingsChanged");
        }
    }
}

Итак, функционал для настройки кнопки готов, осталось добавить реализацию для добавления в качестве ContentPart к типам контента и отображения кнопки.

Добавляем стандартного вида хендер:

public class YandexShareHandler: ContentHandler
{
    public YandexShareHandler(IRepository<YandexShareRecord> repository)
    {
        Filters.Add(StorageFilter.For(repository));
    }
}

И теперь самое главное Driver:

public class YandexShareDriver: ContentPartDriver<YandexSharePart>
{
    IWorkContextAccessor _workAccessor;
    IYandexShareService _yandexShareSerivce;

    public YandexShareDriver(IWorkContextAccessor workAccessor, IYandexShareService yandexShareService)
    {
        _workAccessor = workAccessor;
        _yandexShareSerivce = yandexShareService;
    }

    protected override DriverResult Display(YandexSharePart part, string displayType, dynamic shapeHelper)
    {
        var settings = _yandexShareSerivce.Get(); // получаем настройки
       
        if (settings == null) return null;
        if (!settings.IsEnabled) return null; // включено ли отображение кнопки?

        IResourceManager resourceManager = _workAccessor.GetContext().Resolve<IResourceManager>(); // понадобится для добавления скрипта в хедер сайта
        if (resourceManager != null)
        {
            var scripts = resourceManager.GetRegisteredHeadScripts(); // получаем зарегистрированные в хедере скрипты
            if((scripts!=null && scripts.Where(x=>x.Contains("yandex.st")==true).FirstOrDefault()==null) || scripts==null) // проверяем есть ли в скриптах наш и если нет, то добавляем
                resourceManager.RegisterHeadScript("<script type="text/javascript" src="http://yandex.st/share/share.js" charset="utf-8"></script>");
        }
        string title = string.Empty;
        string link = string.Empty;
        var routePart = part.ContentItem.As<RoutePart>();
        if (routePart != null) // содержит ли текущая отображаемая запись RoutePart, если содержит то получаем заголовок и ссылку
        {
            title = routePart.Title;
            link = string.Format("http://{0}/{1}", HttpContext.Current.Request.Url.Host, routePart.Path);
        }
                // отображаем нашу вью, предварительно передав в неё данные
        return ContentShape("Parts_YandexShare", () => shapeHelper.Parts_YandexShare(
            Title:title,
            Link:link,
            ShareType: settings.ShareType,
            ShareServices: settings.ShareServices,
            ShareLang: settings.ShareLang
            ));
    }
}

А вот так выглядит вью:

<div class="yashare-auto-init" data-yashareTitle="@Model.Title" data-yashareLink="@Model.Link" data-yashareL10n="@Model.ShareLang" data-yashareType="@Model.ShareType" data-yashareQuickServices="@Model.ShareServices"></div>

В конце остаётся добавить файл placement.info с информацией о том, где нужно отображать нашу кнопку

<Placement>
    <Place Parts_YandexShare="Content:10"/>
</Placement>

И описание модуля в файл Module.txt.

Завершается процесс создания модуля как всегда командой

orchard.exe package create YandexShare f:

Вместо корня диска f можно указать свой путь.

Проект с исходным кодом
Модуль на сайте Orchard
Посмотреть как это работает можно тут.

Orchard CMS: пишем модуль для добавления кодов, подтверждающих права на управление сайтом

Программирование

Tagged Under :

Для того, чтобы отслеживать популярные запросы, клики и т.д. и т.п. удобно пользоваться инструментами для веб-мастеров от гугла и яндекс (решил также попробовать бинг), но для этого необходимо подтвердить права собственности на сайт. Предлагается несколько вариантов, для меня более простым выглядит добавление мета-кодов в страницу.

К сожалению, у Orchard CMS нет встроенной возможности добавить такие коды в страницу, а править непосредственно Document.cshtml не хотелось, поэтому я решил сделать специально для этого простенький модуль.

Прежде всего создаём шаблон модуля:

orchard.exe codegen module kosfiz.WebSiteOwner

В папке Models создаём класс

public class WebSiteOwnerRecord
{
    public virtual int Id { get; set; }
    [Required]
    public virtual string Title { get; set; } //название, чтобы было удобно ориентироваться в кодах
    [Required]
    public virtual string MetaName { get; set; } //значение аттрибута name мета-тега
    [Required]
    public virtual string MetaContent { get; set; }//значение аттрибута content мета-тега
}

Все записи нам понадобиться хранить в базе, поэтому добавляем Migrations.cs

orchard.exe codegen datamigration kosfiz.WebSiteOwner

В него добавляем следующее:

public class Migrations : DataMigrationImpl {

    public int Create() {

        SchemaBuilder.CreateTable("WebSiteOwnerRecord", table => table.Column("Id", DbType.Int32, column => column.PrimaryKey().Identity())
            .Column("Title", DbType.String, column=>column.NotNull())
            .Column("MetaName", DbType.String, column=>column.NotNull())
            .Column("MetaContent", DbType.String, column=>column.NotNull()));

        return 1;
    }
}

В итоге при установке создастся таблица со всеми описанными полями.

Для управления (создания, редактирования, удаления) записями необходимо реализовать Service (в контексте Orchard это своеобразный класс, реализующий работу с данными). Добавим к проекту папку Services, а в неё интерфейс и класс, соответственно, IWebSiteOwnerService и WebSiteOwnerService.

public interface IWebSiteOwnerService: IDependency
{
    WebSiteOwnerRecord Get(int Id);
    List<WebSiteOwnerRecord> Get();
    bool Set(int Id, string Title, string MetaName, string MetaContent);
    bool Delete(int Id);
    void Add(string Title, string MetaName, string MetaContent);
}
public class WebSiteOwnerService: IWebSiteOwnerService
{
    IRepository<WebSiteOwnerRecord> _repository;
    ISignals _isignals;
    public WebSiteOwnerService(IRepository<WebSiteOwnerRecord> repository, ISignals signals)
    {
        _isignals = signals;
        _repository = repository;
    }

    public WebSiteOwnerRecord Get(int Id)
    {
        return _repository.Table.Where(x => x.Id == Id).FirstOrDefault();
    }

    public List<WebSiteOwnerRecord> Get()
    {
        return _repository.Table.ToList();
    }

    public bool Set(int Id, string Title, string MetaName, string MetaContent)
    {
        bool result = false;
        var record = Get(Id);
        if (record != null)
        {
            record.Title = Title;
            record.MetaName = MetaName;
            record.MetaContent = MetaContent;
            _isignals.Trigger("kosfiz.WebSiteOwnerRecordChanged");
            return true;
        }

        return result;
    }

    public bool Delete(int Id)
    {
        bool result = false;
        var record = Get(Id);
        if (record != null)
        {
            _repository.Delete(record);
            _isignals.Trigger("kosfiz.WebSiteOwnerRecordChanged");
        }
        return result;
    }

    public void Add(string Title, string MetaName, string MetaContent)
    {
        _isignals.Trigger("kosfiz.WebSiteOwnerRecordChanged");
        _repository.Create(new WebSiteOwnerRecord { Title = Title, MetaName = MetaName, MetaContent = MetaContent });
    }
}

Основную работу можно считать законченной, теперь остаётся добавить пункт меню в панели управления и написать контроллер для создания, удаления, отображения кодов.

Для создания меню к проекту добавляем класс AdminMenu следующего содержания:

public class AdminMenu: INavigationProvider
{
    public Localizer T { get; set; }

    public AdminMenu()
    {
        T = NullLocalizer.Instance;
    }

    public void GetNavigation(NavigationBuilder builder)
    {
        builder.Add(T("WebSiteOwner"), "19", menu=>menu.Add(T("WebSiteOwner"), "0", item=>item.Action("Index", "Admin", new {area = "kosfiz.WebSiteOwner"})));
    }

    public string MenuName
    {
        get { return "admin"; }
    }
}

Здесь определяется в каком меню отображаем наш пункт и определяем контроллер и действие, которое будет отрабатывать при нажатии на пункт меню.

На следующем шаге добавляем контроллер, AdminController, следующего содержания:

[ValidateInput(false), Admin]
public class AdminController: Controller
{
    private readonly IWebSiteOwnerService _webSiteOwnerService;

    public IOrchardServices orchardServices { get; set; }
    public Localizer T { get; set; }

    public AdminController(IWebSiteOwnerService webSiteOwnerService, IOrchardServices Service)
    {
        orchardServices = Service;
        _webSiteOwnerService = webSiteOwnerService;
        T = NullLocalizer.Instance;
    }

    [HttpGet]
    public ActionResult Index()
    {
        var listOfRecords = _webSiteOwnerService.Get();
        if (listOfRecords == null || listOfRecords.Count == 0)
            ViewBag.EmptyMessage = T("No data");
        return View(listOfRecords);
    }

    [HttpGet]
    public ActionResult Create()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Create(CreateEditWebSiteOwnerModel model)
    {
        if (!ModelState.IsValid)
            return View();
        _webSiteOwnerService.Add(model.Title, model.MetaName, model.MetaContent);
        return RedirectToAction("Index");
    }

    [HttpGet]
    public ActionResult Edit(int Id)
    {
        return View(_webSiteOwnerService.Get(Id));
    }

    [HttpPost]
    public ActionResult Edit(int Id, CreateEditWebSiteOwnerModel model)
    {
        if(!ModelState.IsValid)
            return View(_webSiteOwnerService.Get(Id));
        _webSiteOwnerService.Set(Id, model.Title, model.MetaName, model.MetaContent);
        return RedirectToAction("Index");
    }

    [HttpGet]
    public ActionResult Delete(int Id)
    {
        _webSiteOwnerService.Delete(Id);
        return RedirectToAction("Index");
    }
}

В папку Views добавляем папку Admin и в неё вьюшки: Create.cshtml, Index.cshtml, Edit.cshtml. Соответственно, такие:

<h1>@Html.TitleForPage(T("Create new ownership verification code").ToString())</h1>
<div class="add-code">
@Html.ActionLink("Back to list", "Index")
</div>
@using (Html.BeginFormAntiForgeryPost())
{
    @Html.ValidationSummary()
    <fieldset>
    <p><label for="Title">@T("Title")</label>
    <input id="Title" name="Title" type="text" />
    </p>
    <p><label for="MetaName">@T("Name")</label>
    <input id="MetaName" name="MetaName" type="text" />
    </p>
    <p><label for="MetaContext">@T("Content")</label>
    <input id="MetaContent" name="MetaContent" type="text" />
    </p>
    <input type="submit" value="Save" />
    </fieldset>
}
@model List<kosfiz.WebSiteOwner.Models.WebSiteOwnerRecord>
@{
    Style.Require("WebSiteOwnerStyle");
}
<h1>@Html.TitleForPage(T("Ownership verification codes").ToString())</h1>
<div class="add-code">
@Html.ActionLink(T("Create").ToString(), "Create")
</div>
@ViewBag.EmptyMessage
@if (Model != null && Model.Count > 0)
{
   @*<a href="@Url.Action("Create", "Admin")">@T("Create")</a>*@
   <table cellpadding="0" cellspacing="0" class="codes-list">
   <tr>
       <th>@T("Title")</th>
       <th class="code-action">@T("Actions")</th>
   </tr>
   @foreach (var item in Model)
   {
       <tr>
           <td><a href="@Url.Action("Edit", "Admin", new { Id = item.Id })">@item.Title</a></td>
           <td>
               <a href="@Url.Action("Edit", "Admin", new { Id = item.Id })">@T("Edit")</a> | <a href="@Url.Action("Delete", "Admin", new { Id = item.Id })">@T("Remove")</a>
           </td>
       </tr>
   }

   </table>
}
@model kosfiz.WebSiteOwner.Models.WebSiteOwnerRecord

<h1>@Html.TitleForPage(T("Edit ownership verification code").ToString())</h1>
<div class="add-code">
@Html.ActionLink("Back to list", "Index")
</div>
@using (Html.BeginFormAntiForgeryPost())
{
    @Html.ValidationSummary()
    <fieldset>
    <p><label for="Title">@T("Title")</label>
    <input id="Title" name="Title" type="text" value="@Model.Title" />
    </p>
    <p><label for="MetaName">@T("Name")</label>
    <input id="MetaName" name="MetaName" type="text" value="@Model.MetaName" />
    </p>
    <p><label for="MetaContext">@T("Content")</label>
    <input id="MetaContent" name="MetaContent" type="text" value="@Model.MetaContent" />
    </p>
    <input type="submit" value="Save" />
    </fieldset>
}

Выше в коде мелькал класс CreateEditWebSiteOwnerModel, который используется при создании и обновлении мета-кодов. Файл, описывающий данный класс, я расположил в папке ViewModels. Он имеет весьма простой код:

public class CreateEditWebSiteOwnerModel
{
    [Required]
    public string Title { get; set; }
    [Required]
    public string MetaName { get; set; }
    [Required]
    public string MetaContent { get; set; }
}

У полей стоят аттрибуты Required, что говорит о том, что все поля обязательны. Использование такого подхода (с созданием ViewModel для данных приходящих со страницы) позволяет достаточно сильно упростить процесс валидации сведя его к коду:

if(!ModelState.IsValid)
 //делаем то, что нужно для случая когда данные некорректны

Теперь почти всё готово: есть заготовка для таблицы, описана запись, есть управление и меню, но не реализовано добавление непосредственно проверочных мета-кодов в страницу. Чтобы это исправить добавляем к проекту папку Filters, а в неё класс WebSiteOwnerFilter:

public class WebSiteOwnerFilter: FilterProvider, IResultFilter
{
    private readonly IWorkContextAccessor _workContextAccessor;
    private readonly IWebSiteOwnerService _webSiteOwnerService;
    private readonly ICacheManager _cacheManager;
    private readonly ISignals _signals;

    public WebSiteOwnerFilter(
        IWorkContextAccessor workContextAccessor,
        IWebSiteOwnerService webSiteOwnerService,
        ICacheManager cacheManager,
        ISignals signals) {
        _workContextAccessor = workContextAccessor;
        _webSiteOwnerService = webSiteOwnerService;
        _cacheManager = cacheManager;
        _signals = signals;
    }

    public void OnResultExecuted(ResultExecutedContext filterContext)
    {
       
    }

    public void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (AdminFilter.IsApplied(filterContext.RequestContext))
            return; //если админка, то выходим
       
        IResourceManager resourceManager = _workContextAccessor.GetContext().Resolve<IResourceManager>(); //получаем экземпляр ресурс менеджера, он нужен для работы с метой, скриптами и стилями

        var metas = _cacheManager.Get("kosfiz.WebSiteOwner", ctx =>
        {
            ctx.Monitor(_signals.When("kosfiz.WebSiteOwnerRecordChanged"));
            var _metas = _webSiteOwnerService.Get();
            return _metas;
        }); //получаем все мета-коды по возможности из кеша

        foreach (var item in metas)
            resourceManager.SetMeta(new MetaEntry { Name = item.MetaName, Content = item.MetaContent }); //добавляем коды в страницу
    }
}

В конструкторе получаем всё то, что может пригодится, но основное оставляем для метода OnResultExecuting. Там мы получаем записи для добавления в страницу, используя кеширование, и используя метод SetMeta добавляем их в неё.

Создаём пакет

orchard.exe package create kosfiz.WebSiteOwner

Вот и всё.

Скрин:

Исходный код
Модуль WebSiteOwner
Страница модуля в галерее Orchard