Birden fazla eklenti dizini ekle


39

Görev

register_theme_directory()WP kurulumunuz için ek Temalar dizinleri ekleyerek kayıt olabilirsiniz . Ne yazık ki çekirdek, eklentiler için aynı işlevselliği sunmuyor. Zaten Eklenti, Eklentiler, Eklentiler ve Temalara sahibiz. Ancak daha iyi bir dosya organizasyonu için daha fazlasına ihtiyacımız var.

İşte elde edilecek görevlerin listesi:

  • Ek bir eklenti dizini ekleyin
  • Her eklenti dizini için, burada gösterildiği gibi yeni bir "sekme" gereklidir [1]
  • Ek dizin, varsayılan eklenti dizininin sahip olduğu işlevlerle aynı işlevi görür.

Senin için orada ne var?

En iyi ve en eksiksiz cevap bir lütufla ödüllendirilecektir.


[1] Yeni bir eklenti klasörü / dizini için ek sekme


3
Dizin yapısı oldukça dizin sabitlerine bağlı olduğundan, bunu dosya sistemi düzeyinde yapmanın pratik olduğunu (çekirdeğin benimsenmesi olmadan) şüpheliyim. Yönetici içindeki sanal organizasyon katmanının genişletme düzeyinde elde edilmesi daha kolay olabilir.
Rarst

@Rarst Düşüncelerinizi eklemekten sizi alıkoymaması gereken :)
kaiser

Bu harika bir özellik olurdu.
ltfishie

Özellik iyi geliyor. Sadece mühendislik çekirdeğini tersine çevirmeniz, nasıl yapılması gerektiğini (WP yolu) bulmanız ve ardından Dev'lere bir yama göndermeniz gerekiyor ... register_theme_directory () - search_theme_directories () - get_raw_theme_root () - get_theme_roots () - get_theme () - get_themes ()
Sterling Hamilton

2
Çocuklar: Neyi gönderin ? Bu bir soru, tam kod çözülmüş bir cevap değil :) FYI: Birget_themes() sınıfa yeniden yazmak için trac'te yeni bir bilet .
kaiser

Yanıtlar:


28

Tamam, bunu bir bıçaklayacağım. Yol boyunca karşılaştığım bazı sınırlamalar:

  1. WP_List_Table alt sınıflarında çok fazla filtre yok, en azından hiç olmaları gereken yerde değil.

  2. Bu filtre eksikliği nedeniyle, en üstte doğru bir eklenti türü listesi tutamıyoruz.

  3. Ayrıca eklentileri aktif olarak görüntülemek için bazı harika (okuma: kirli) JavaScript kodları kullanmamız gerekiyor.

Yönetici alan kodumu bir sınıfın içine sardım, bu yüzden işlev isimlerim ön ekli değil. Bu kodun tamamını burada görebilirsiniz . Lütfen katkıda bulunun!

Merkez API

Eklenti dizinlerimizi ilişkisel bir dizide içerecek global bir değişken oluşturan basit bir işlev. $keyVb eklentileri almak için dahili olarak kullanılan bir şey olacak $dirya tam bir yol ya da bir şey görecelidir wp-contentdizine. $labeladmin alanındaki gösterimiz için olacak (örneğin çevrilebilir bir dize).

<?php
function register_plugin_directory( $key, $dir, $label )
{
    global $wp_plugin_directories;
    if( empty( $wp_plugin_directories ) ) $wp_plugin_directories = array();

    if( ! file_exists( $dir ) && file_exists( trailingslashit( WP_CONTENT_DIR ) . $dir ) )
    {
        $dir = trailingslashit( WP_CONTENT_DIR ) . $dir;
    }

    $wp_plugin_directories[$key] = array(
        'label' => $label,
        'dir'   => $dir
    );
}

O zaman elbette eklentileri yüklemeliyiz. İçine Hook plugins_loadedşekilde geç ve her yükleme, aktif eklentileri geçmesi.

Yönetici Alanı

İşlevselliğimizi bir sınıf içinde kuralım.

<?php
class CD_APD_Admin
{

    /**
     * The container for all of our custom plugins
     */
    protected $plugins = array();

    /**
     * What custom actions are we allowed to handle here?
     */
    protected $actions = array();

    /**
     * The original count of the plugins
     */
    protected $all_count = 0;

    /**
     * constructor
     * 
     * @since 0.1
     */
    function __construct()
    {
        add_action( 'load-plugins.php', array( &$this, 'init' ) );
        add_action( 'plugins_loaded', array( &$this, 'setup_actions' ), 1 );

    }

} // end class

Biz içine kanca gidiyoruz plugins_loadedgerçekten erken ve kullanacağımız izin "eylemleri" kurdu. Bunlar, yerleşik işlevler özel dizinlerle yapamadığından eklenti etkinleştirme ve devre dışı bırakma işlemlerini gerçekleştirir.

function setup_actions()
{
    $tmp = array(
        'custom_activate',
        'custom_deactivate'
    );
    $this->actions = apply_filters( 'custom_plugin_actions', $tmp );
}

Sonra bağlı fonksiyonu var load-plugins.php. Bu her türlü eğlenceli şeyi yapar.

function init()
{
    global $wp_plugin_directories;

    $screen = get_current_screen();

    $this->get_plugins();

    $this->handle_actions();

    add_filter( 'views_' . $screen->id, array( &$this, 'views' ) );

    // check to see if we're using one of our custom directories
    if( $this->get_plugin_status() )
    {
        add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
        add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
        // TODO: support bulk actions
        add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
        add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
        add_action( 'admin_enqueue_scripts', array( &$this, 'scripts' ) );
    }
}

Her seferinde bir şeyi geçelim. get_pluginsyöntem olup, bir fonksiyon etrafında bir sarıcı. Özniteliği pluginsveriyle doldurur .

function get_plugins()
{
    global $wp_plugin_directories;
    foreach( array_keys( $wp_plugin_directories ) as $key )
    {
       $this->plugins[$key] = cd_apd_get_plugins( $key );
    }
}

cd_apd_get_pluginsget_pluginskodlanmış WP_CONTENT_DIRve işsiz yerleşik fonksiyonun bir yırtıktır plugins. Temel olarak: dizini $wp_plugin_directoriesglobalden alın, açın, tüm eklenti dosyalarını bulun. Daha sonra bunları önbellekte saklayın.

<?php
function cd_apd_get_plugins( $dir_key ) 
{
    global $wp_plugin_directories;

    // invalid dir key? bail
    if( ! isset( $wp_plugin_directories[$dir_key] ) )
    {
        return array();
    }
    else
    {
        $plugin_root = $wp_plugin_directories[$dir_key]['dir'];
    }

    if ( ! $cache_plugins = wp_cache_get( 'plugins', 'plugins') )
        $cache_plugins = array();

    if ( isset( $cache_plugins[$dir_key] ) )
        return $cache_plugins[$dir_key];

    $wp_plugins = array();

    $plugins_dir = @ opendir( $plugin_root );
    $plugin_files = array();
    if ( $plugins_dir ) {
        while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
            if ( substr($file, 0, 1) == '.' )
                continue;
            if ( is_dir( $plugin_root.'/'.$file ) ) {
                $plugins_subdir = @ opendir( $plugin_root.'/'.$file );
                if ( $plugins_subdir ) {
                    while (($subfile = readdir( $plugins_subdir ) ) !== false ) {
                        if ( substr($subfile, 0, 1) == '.' )
                            continue;
                        if ( substr($subfile, -4) == '.php' )
                            $plugin_files[] = "$file/$subfile";
                    }
                    closedir( $plugins_subdir );
                }
            } else {
                if ( substr($file, -4) == '.php' )
                    $plugin_files[] = $file;
            }
        }
        closedir( $plugins_dir );
    }

    if ( empty($plugin_files) )
        return $wp_plugins;

    foreach ( $plugin_files as $plugin_file ) {
        if ( !is_readable( "$plugin_root/$plugin_file" ) )
            continue;

        $plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false ); //Do not apply markup/translate as it'll be cached.

        if ( empty ( $plugin_data['Name'] ) )
            continue;

        $wp_plugins[trim( $plugin_file )] = $plugin_data;
    }

    uasort( $wp_plugins, '_sort_uname_callback' );

    $cache_plugins[$dir_key] = $wp_plugins;
    wp_cache_set('plugins', $cache_plugins, 'plugins');

    return $wp_plugins;
}

Bir sonraki adım, eklentileri gerçekten etkinleştirip devre dışı bırakmanın sinir bozucu işi. Bunu yapmak için handle_actionsyöntemi kullanıyoruz. Bu, yine, çekirdek wp-admin/plugins.phpdosyanın üstünden açıkça sökülüp atılmıştır .

function handle_actions()
{
    $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';

    // not allowed to handle this action? bail.
    if( ! in_array( $action, $this->actions ) ) return;

    // Get the plugin we're going to activate
    $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : false;
    if( ! $plugin ) return;

    $context = $this->get_plugin_status();

    switch( $action )
    {
        case 'custom_activate':
            if( ! current_user_can('activate_plugins') )
                    wp_die( __('You do not have sufficient permissions to manage plugins for this site.') );

            check_admin_referer( 'custom_activate-' . $plugin );

            $result = cd_apd_activate_plugin( $plugin, $context );
            if ( is_wp_error( $result ) ) 
            {
                if ( 'unexpected_output' == $result->get_error_code() ) 
                {
                    $redirect = add_query_arg( 'plugin_status', $context, self_admin_url( 'plugins.php' ) );
                    wp_redirect( add_query_arg( '_error_nonce', wp_create_nonce( 'plugin-activation-error_' . $plugin ), $redirect ) ) ;
                    exit();
                } 
                else 
                {
                    wp_die( $result );
                }
            }

            wp_redirect( add_query_arg( array( 'plugin_status' => $context, 'activate' => 'true' ), self_admin_url( 'plugins.php' ) ) );
            exit();
            break;
        case 'custom_deactivate':
            if ( ! current_user_can( 'activate_plugins' ) )
                wp_die( __('You do not have sufficient permissions to deactivate plugins for this site.') );

            check_admin_referer('custom_deactivate-' . $plugin);
            cd_apd_deactivate_plugins( $plugin, $context );
            if ( headers_sent() )
                echo "<meta http-equiv='refresh' content='" . esc_attr( "0;url=plugins.php?deactivate=true&plugin_status=$status&paged=$page&s=$s" ) . "' />";
            else
                wp_redirect( self_admin_url("plugins.php?deactivate=true&plugin_status=$context") );
            exit();
            break;
        default:
            do_action( 'custom_plugin_dir_' . $action );
            break;
    }

}

Birkaç özel burada tekrar çalışır. cd_apd_activate_plugin(koparıldı activate_plugin) ve cd_apd_deactivate_plugins(koparıldı deactivate_plugins). Her ikisi de, kodlanmış dizinleri olmayan ilgili "ebeveyn" işlevleriyle aynıdır.

function cd_apd_activate_plugin( $plugin, $context, $silent = false ) 
{
    $plugin = trim( $plugin );

    $redirect = add_query_arg( 'plugin_status', $context, admin_url( 'plugins.php' ) );
    $redirect = apply_filters( 'custom_plugin_redirect', $redirect );

    $current = get_option( 'active_plugins_' . $context, array() );

    $valid = cd_apd_validate_plugin( $plugin, $context );
    if ( is_wp_error( $valid ) )
        return $valid;

    if ( !in_array($plugin, $current) ) {
        if ( !empty($redirect) )
            wp_redirect(add_query_arg('_error_nonce', wp_create_nonce('plugin-activation-error_' . $plugin), $redirect)); // we'll override this later if the plugin can be included without fatal error
        ob_start();
        include_once( $valid );

        if ( ! $silent ) {
            do_action( 'custom_activate_plugin', $plugin, $context );
            do_action( 'custom_activate_' . $plugin, $context );
        }

        $current[] = $plugin;
        sort( $current );
        update_option( 'active_plugins_' . $context, $current );

        if ( ! $silent ) {
            do_action( 'custom_activated_plugin', $plugin, $context );
        }

        if ( ob_get_length() > 0 ) {
            $output = ob_get_clean();
            return new WP_Error('unexpected_output', __('The plugin generated unexpected output.'), $output);
        }
        ob_end_clean();
    }

    return true;
}

Ve deaktivasyon işlevi

function cd_apd_deactivate_plugins( $plugins, $context, $silent = false ) {
    $current = get_option( 'active_plugins_' . $context, array() );

    foreach ( (array) $plugins as $plugin ) 
    {
        $plugin = trim( $plugin );
        if ( ! in_array( $plugin, $current ) ) continue;

        if ( ! $silent )
            do_action( 'custom_deactivate_plugin', $plugin, $context );

        $key = array_search( $plugin, $current );
        if ( false !== $key ) {
            array_splice( $current, $key, 1 );
        }

        if ( ! $silent ) {
            do_action( 'custom_deactivate_' . $plugin, $context );
            do_action( 'custom_deactivated_plugin', $plugin, $context );
        }
    }

    update_option( 'active_plugins_' . $context, $current );
}

cd_apd_validate_pluginElbette, validate_pluginzor kodlanmış hurdaların olmadığı bir paramparça olan fonksiyon da var .

<?php
function cd_apd_validate_plugin( $plugin, $context ) 
{
    $rv = true;
    if ( validate_file( $plugin ) )
    {
        $rv = new WP_Error('plugin_invalid', __('Invalid plugin path.'));
    }

    global $wp_plugin_directories;
    if( ! isset( $wp_plugin_directories[$context] ) )
    {
        $rv = new WP_Error( 'invalid_context', __( 'The context for this plugin does not exist' ) );
    }

    $dir = $wp_plugin_directories[$context]['dir'];
    if( ! file_exists( $dir . '/' . $plugin) )
    {
        $rv = new WP_Error( 'plugin_not_found', __( 'Plugin file does not exist.' ) );
    }

    $installed_plugins = cd_apd_get_plugins( $context );
    if ( ! isset($installed_plugins[$plugin]) )
    {
        $rv = new WP_Error( 'no_plugin_header', __('The plugin does not have a valid header.') );
    }

    $rv = $dir . '/' . $plugin;
    return $rv;
}

Tamam, bunun dışında. Liste tablosunun görüntüsü hakkında konuşmaya başlayabiliriz.

1. Adım: görünümlerimizi tablonun en üstündeki listeye ekleyin. Bu, fonksiyonumuza filtre views_{$screen->id}uygulayarak yapılır init.

add_filter( 'views_' . $screen->id, array( &$this, 'views' ) );

Daha sonra gerçek çengel işlevi sadece içinden geçiyor $wp_plugin_directories. Yeni kayıtlı dizinlerden birinin eklentileri varsa, ekrana ekleriz.

function views( $views )
{
    global $wp_plugin_directories;

    // bail if we don't have any extra dirs
    if( empty( $wp_plugin_directories ) ) return $views;

    // Add our directories to the action links
    foreach( $wp_plugin_directories as $key => $info )
    {
        if( ! count( $this->plugins[$key] ) ) continue;
        $class = $this->get_plugin_status() == $key ? ' class="current" ' : '';
        $views[$key] = sprintf( 
            '<a href="%s"' . $class . '>%s <span class="count">(%d)</span></a>',
            add_query_arg( 'plugin_status', $key, 'plugins.php' ),
            esc_html( $info['label'] ),
            count( $this->plugins[$key] )
        );
    }
    return $views;
}

Özel bir eklenti dizini sayfasını görüntülüyor olmamız durumunda yapmanız gereken ilk şey, görünümleri yeniden filtrelemek. Sayıdan kurtulmamız gerekiyor, inactiveçünkü doğru olmayacak. Bunun bir sonucu olarak, ihtiyaç duyduğumuz yerde hiçbir filtre olmamıştır. Tekrar bağla ...

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
}

Ve hızlı bir üzücü ...

function views_again( $views )
{
    if( isset( $views['inactive'] ) ) unset( $views['inactive'] );
    return $views;
}

Daha sonra, liste tablosunda göreceğiniz eklentilerden kurtulalım ve bunları özel eklentilerimizle değiştirelim. İçine kanca all_plugins.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
}

Eklentilerimizi ve verilerimizi zaten kurduğumuzdan (yukarıya bakın setup_plugins), filter_pluginssadece (1) yöntemi tüm eklentilerdeki sayımı daha sonra kaydeder ve (2) liste tablosundaki eklentileri değiştirir.

function filter_plugins( $plugins )
{
    if( $key = $this->get_plugin_status() )
    {
        $this->all_count = count( $plugins );
        $plugins = $this->plugins[$key];
    }
    return $plugins;
}

Ve şimdi toplu eylemleri öldüreceğiz. Bunlar kolayca desteklenebilir, sanırım?

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
}

Varsayılan eklenti işlemleri bağlantıları bizim için çalışmayacak. Bunun yerine, kendimizi (özel eylemlerle vb.) Kurmamız gerekiyor. In initfonksiyonu.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
    add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
}

Burada değiştirilen tek şey (1) eylemleri değiştiriyoruz, (2) eklenti durumunu koruyor ve (3) olmayan isimleri biraz değiştiriyoruz.

function action_links( $links, $plugin_file )
{
    $context = $this->get_plugin_status();

    // let's just start over
    $links = array();
    $links['activate'] = sprintf(
        '<a href="%s" title="Activate this plugin">%s</a>',
        wp_nonce_url( 'plugins.php?action=custom_activate&amp;plugin=' . $plugin_file . '&amp;plugin_status=' . esc_attr( $context ), 'custom_activate-' . $plugin_file ),
        __( 'Activate' )
    );

    $active = get_option( 'active_plugins_' . $context, array() );
    if( in_array( $plugin_file, $active ) )
    {
        $links['deactivate'] = sprintf(
            '<a href="%s" title="Deactivate this plugin" class="cd-apd-deactivate">%s</a>',
            wp_nonce_url( 'plugins.php?action=custom_deactivate&amp;plugin=' . $plugin_file . '&amp;plugin_status=' . esc_attr( $context ), 'custom_deactivate-' . $plugin_file ),
            __( 'Deactivate' )
        );
    }
    return $links;
}

Ve son olarak, üstesinden gelmek için biraz JavaScript kullanmamız gerekiyor. In initfonksiyonu tekrar (hep birlikte bu kez).

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
    add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
    add_action( 'admin_enqueue_scripts', array( &$this, 'scripts' ) );
}

JS'imizi zorlarken wp_localize_scripttoplam "tüm eklentiler" sayısının değerini almak için de kullanırız .

function scripts()
{
    wp_enqueue_script(
        'cd-apd-js',
        CD_APD_URL . 'js/apd.js',
        array( 'jquery' ),
        null
    );
    wp_localize_script(
        'cd-apd-js',
        'cd_apd',
        array(
            'count' => esc_js( $this->all_count )
        )
    );
}

Ve elbette, JS liste tablosunu etkin / etkin olmayan eklentilerin düzgün şekilde görüntülemesini sağlamak için sadece bazı hoş hack'lerdir. Ayrıca tüm eklentilerin doğru sayısını tekrar Allbağlantıya yapıştırırız .

jQuery(document).ready(function(){
    jQuery('li.all a').removeClass('current').find('span.count').html('(' + cd_apd.count + ')');
    jQuery('.wp-list-table.plugins tr').each(function(){
        var is_active = jQuery(this).find('a.cd-apd-deactivate');
        if(is_active.length) {
            jQuery(this).removeClass('inactive').addClass('active');
            jQuery(this).find('div.plugin-version-author-uri').removeClass('inactive').addClass('active');
        }
    });
});

Sarmak

Eklenti dizinlerinin gerçek yüklenmesi oldukça heyecan vericidir. Liste tablosunun doğru görüntülenmesini sağlamak daha zor olan kısımdır. Yine de sonuçlarından tamamen memnun değilim, ancak belki birileri kodu iyileştirebilir


1
Etkileyici! Gerçekten iyi iş. Kodunu incelemek için haftasonu biraz zaman geçireceğim. Not: Bir fonksiyon var __return_empty_array().
fuxia

Teşekkürler! Geribildirim her zaman beklerim. Incorporated __return_empty_arrayişlevi!
chrisguitarguy

1
Basit bir çekirdek filtrenin size ayrı bir işlev kazandırdığı tüm yerlerin bir listesini toplamalısınız. Ve sonra… bir Trac bileti gönderin.
fuxia

Bu gerçekten harika. Bunu bir Temanın içinde bir kütüphane olarak yapabilsek daha iyi olurdu (Github: github.com/chrisguitarguy/WP-Plugin-Directories/issues/4 hakkındaki yorumuma bakın )
julien_c

1
+1 Bu cevabı özlediğime inanamıyorum - harika iş! Hafta sonundaki kodunu daha detaylı inceleyeceğim :). @Julien_c - neden bunu bir temanın içinde kullandın?
Stephen Harris

2

Kişisel olarak UI’yi değiştirmekle ilgilenmiyorum, ancak çeşitli nedenlerden dolayı daha organize bir dosya sistemi düzenini çok isterim.

Bu amaçla, başka bir yaklaşım da sembolik bağlantılar kullanmak olacaktır.

wp-content
    |-- plugins
        |-- acme-widgets               -> ../plugins-custom/acme-widgets
        |-- acme-custom-post-types     -> ../plugins-custom/acme-custom-post-types
        |-- acme-business-logic        -> ../plugins-custom/acme-business-logic
        |-- google-authenticator       -> ../plugins-external/google-authenticator
        |-- rest-api                   -> ../plugins-external/rest-api
        |-- quick-navigation-interface -> ../plugins-external/quick-navigation-interface
    |-- plugins-custom
        |-- acme-widgets
        |-- acme-custom-post-types
        |-- acme-business-logic
    |-- plugins-external
        |-- google-authenticator
        |-- rest-api
        |-- quick-navigation-interface

plugins-customProjenizin sürüm kontrol deposunun bir parçası olabilecek özel eklentilerinizi ayarlayabilirsiniz .

Ardından 3. taraf bağımlılıklarını plugins-external(Composer veya Git alt modülleri veya tercih ettiğiniz şekilde) kurabilirsiniz .

Ardından ek dizinleri tarayan ve pluginsbulduğu her alt klasör için bir sembolik bağlantı oluşturan basit bir Bash betiğine veya WP-CLI komutuna sahip olabilirsiniz .

pluginshala darmadağın olurdu, ama önemli değil çünkü sadece plugins-customve ile etkileşime girmen gerekiyor plugins-external.

nEkstra dizinlere ölçeklendirme , ilk ikisi ile aynı işlemi izler.


-3

Veya COMPOSER'ı, wp-content klasörüne işaret edecek şekilde ayarlanmış özel dizin yolu ile de kullanabilirsiniz. Sorunuzun doğrudan bir cevabı değilse, wordpress düşünmenin yeni bir yolu ise, sizi yemeden önce besteciye geçin.


Besteci uzun zaman önce taşındı. Lütfen bu sorunun tarihine bakınız. Bunun dışında: Bu gerçekten bir cevap değil. Belki bunu nasıl ayarlayacağımı gösterebilirim?
kaiser
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.