# -*- coding: utf-8 -*-
"""#14 yetim-sayfa-tespiti  |  Site haritası envanteri ile iç bağlantı çıktısını karşılaştıran yetim sayfa denetimi

Yalnız standart kütüphane. Kullanım:
    python -X utf8 yetim-sayfa-tespiti.py https://yademreklam.com https://yademreklam.com/wp-sitemap.xml [cikti.json] [--rest]

Ne yapar
1) Site haritasındaki (sitemap index dahil) adresleri ENVANTER olarak okur; --rest verilirse WordPress REST'teki yayınlanmış adresleri de karşılaştırır.
2) Ana sayfadan başlayıp aynı siteye giden <a href> bağlantılarını izler (envanterdeki ve bulunan tüm HTML sayfaları gezer).
3) Her bağlantıyı bağlamına göre ayırır: içerik (main/article) ya da gezinme (header, nav, footer, aside, menü/widget sınıfları).
4) Envanterdeki her adres için başka sayfalardan gelen bağlantıyı sayar (kendine verilen bağlantı sayılmaz).
Çıktı: yetim (hiç gelen bağlantı yok), yalnız gezinme bağlantılı (içerikten bağlanmayan), tek içerik bağlantılı sayfalar,
site haritasında olmayıp bağlantısı bulunan adresler ve her yetim adayının robots meta değeri.

Sınırlar: JavaScript çalıştırmaz (yalnız sunucunun gönderdiği HTML); robots.txt'e bakmaz; parametreli adresler ve dosyalar atlanır.
"""
import json
import re
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from collections import defaultdict
from html.parser import HTMLParser

UA = {'User-Agent': 'Mozilla/5.0 (compatible; YademYetimDenetim/1.0)'}
VOID = {'br', 'img', 'input', 'meta', 'link', 'hr', 'source', 'area', 'base', 'col', 'embed', 'param', 'track', 'wbr'}
NAV_TAGS = {'nav', 'header', 'footer', 'aside'}
NAV_CLASS = re.compile(r'(menu|nav|footer|header|breadcrumb|crumbs|widget|sidebar|toc)', re.I)
ATLA = re.compile(r'\.(jpe?g|png|gif|webp|svg|pdf|zip|xml|css|js|json|txt|mp4|ico)$|/(wp-content|wp-json|wp-admin|feed|xmlrpc|wp-login)', re.I)


def indir(url):
    r = urllib.request.Request(url, headers=UA)
    try:
        with urllib.request.urlopen(r, timeout=40) as x:
            return x.status, x.geturl(), x.read().decode('utf-8', 'replace')
    except urllib.error.HTTPError as e:
        return e.code, url, ''
    except Exception:
        return 0, url, ''


def normalle(url, host):
    p = urllib.parse.urlparse(url)
    if p.scheme not in ('http', 'https') or p.netloc.replace('www.', '') != host.replace('www.', ''):
        return None
    yol = p.path or '/'
    if ATLA.search(yol) or p.query:
        return None
    if not yol.endswith('/') and '.' not in yol.rsplit('/', 1)[-1]:
        yol += '/'
    return 'https://%s%s' % (host, yol)


class Baglanti(HTMLParser):
    def __init__(self, taban, host):
        super().__init__(convert_charrefs=True)
        self.taban, self.host, self.yigin, self.bagli = taban, host, [], []

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag not in VOID:
            self.yigin.append((tag, a.get('class', '') + ' ' + a.get('id', '')))
        if tag == 'a' and a.get('href'):
            hedef = normalle(urllib.parse.urljoin(self.taban, a['href'].split('#')[0]), self.host)
            if hedef:
                gezinme = any(t in NAV_TAGS or NAV_CLASS.search(c) for t, c in self.yigin)
                icerik = any(t in ('main', 'article') for t, c in self.yigin)
                self.bagli.append((hedef, 'icerik' if icerik and not gezinme else 'gezinme'))

    def handle_endtag(self, tag):
        for i in range(len(self.yigin) - 1, -1, -1):
            if self.yigin[i][0] == tag:
                del self.yigin[i:]
                break


def site_haritasi(url, derinlik=0):
    durum, _, govde = indir(url)
    adresler = re.findall(r'<loc>\s*([^<\s]+)\s*</loc>', govde)
    if '<sitemapindex' in govde and derinlik < 3:
        return [u for alt in adresler for u in site_haritasi(alt, derinlik + 1)]
    return adresler


def rest_adresleri(taban, host):
    """WordPress REST: yayınlanmış sayfa ve yazı adresleri (herkese açık)."""
    out = set()
    for tur in ('pages', 'posts'):
        sayfa = 1
        while True:
            durum, _, g = indir('%s/wp-json/wp/v2/%s?per_page=100&page=%d&_fields=link' % (taban, tur, sayfa))
            if durum != 200:
                break
            veri = json.loads(g)
            out |= {normalle(x['link'], host) for x in veri}
            if len(veri) < 100:
                break
            sayfa += 1
    return out - {None}


def denetle(taban, harita, rest=False, bekle=0.25):
    host = urllib.parse.urlparse(taban).netloc
    envanter = sorted({normalle(u, host) for u in site_haritasi(harita)} - {None})
    rest_farki = sorted(rest_adresleri(taban, host) - set(envanter)) if rest else []
    sirada, gorulen, gelen, durumlar = [normalle(taban + '/', host)] + envanter + rest_farki, set(), defaultdict(lambda: defaultdict(set)), {}
    while sirada:
        u = sirada.pop(0)
        if u in gorulen:
            continue
        gorulen.add(u)
        durum, son, govde = indir(u)
        robots = re.search(r'<meta[^>]{0,200}name="robots"[^>]{0,200}content="([^"]*)"', govde, re.I)
        durumlar[u] = {'durum': durum, 'son_adres': son, 'robots': robots.group(1) if robots else None}
        if durum != 200 or not govde:
            continue
        p = Baglanti(u, host)
        p.feed(govde)
        for hedef, tur in p.bagli:
            if hedef != u:
                gelen[hedef][tur].add(u)
                if hedef not in gorulen:
                    sirada.append(hedef)
        time.sleep(bekle)
    sayilar = lambda u: {'icerik_gelen': len(gelen[u]['icerik']), 'gezinme_gelen': len(gelen[u]['gezinme']),
                         'toplam_gelen': len(gelen[u]['icerik'] | gelen[u]['gezinme']), 'durum': durumlar.get(u, {}).get('durum'), 'robots': durumlar.get(u, {}).get('robots')}
    kume = set(envanter)
    return {'taban': taban, 'envanter': len(envanter), 'gezilen': len(gorulen),
            'satirlar': [{'url': u, **sayilar(u)} for u in envanter],
            'haritada_yok': [{'url': u, **sayilar(u)} for u in sorted((set(gelen) | set(rest_farki)) - kume)]}


if __name__ == '__main__':
    arg = [x for x in sys.argv[1:] if not x.startswith('--')]
    s = denetle(arg[0].rstrip('/'), arg[1], rest='--rest' in sys.argv)
    L = s['satirlar']
    print('envanter %d, gezilen %d' % (s['envanter'], s['gezilen']))
    print('yetim (gelen bağlantı 0):', [(x['url'], x['robots']) for x in L if x['toplam_gelen'] == 0])
    print('yalnız gezinme bağlantılı:', [x['url'] for x in L if x['toplam_gelen'] > 0 and x['icerik_gelen'] == 0])
    print('tek içerik bağlantılı:', [x['url'] for x in L if x['icerik_gelen'] == 1])
    print('haritada yok:', [(x['url'], x['toplam_gelen'], x['robots']) for x in s['haritada_yok']])
    print('durumu 200 olmayan:', [(x['url'], x['durum']) for x in L if x['durum'] != 200])
    if len(arg) > 2:
        json.dump(s, open(arg[2], 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
