div class="ui attached table segment">
@@ -0,0 +1,29 @@
1
+from abc import ABC, abstractmethod
2
+
3
+class PostRepository(ABC):
4
+    
5
+    @abstractmethod
6
+    def list(self, page=1, per_page=10, order_by="-created"): pass
7
+    
8
+    @abstractmethod
9
+    def add(self, post): pass
10
+
11
+    @abstractmethod
12
+    def get(self, post_id): pass
13
+
14
+    @abstractmethod
15
+    def update(self, post):
16
+        """Update an existing post."""
17
+        pass
18
+
19
+    @abstractmethod
20
+    def delete(self, post_id):
21
+        """Delete a post by its ID."""
22
+        pass
23
+
24
+
25
+    @abstractmethod
26
+    def search(self, query: str): pass
27
+
28
+    @abstractmethod
29
+    def rate_post(self, post_id, user_id, rating: int): pass

+ 11 - 0
commu/interfaces/user_repository.py

@@ -0,0 +1,11 @@
1
+from abc import ABC, abstractmethod
2
+
3
+class UserRepository(ABC):
4
+    @abstractmethod
5
+    def add(self, user): pass
6
+
7
+    @abstractmethod
8
+    def get(self, user_id): pass
9
+
10
+    @abstractmethod
11
+    def find_by_username(self, username): pass

+ 0 - 0
commu/tests/__init__.py


BIN
commu/tests/__pycache__/__init__.cpython-310.pyc


BIN
commu/tests/__pycache__/__init__.cpython-311.pyc


BIN
commu/tests/__pycache__/test_post_repository.cpython-310-pytest-8.4.1.pyc


BIN
commu/tests/__pycache__/test_post_repository.cpython-311.pyc


BIN
commu/tests/__pycache__/test_post_service.cpython-310-pytest-8.4.1.pyc


BIN
commu/tests/__pycache__/test_post_service.cpython-311.pyc


BIN
commu/tests/__pycache__/test_user_service.cpython-310-pytest-8.4.1.pyc


BIN
commu/tests/__pycache__/test_user_service.cpython-311.pyc


+ 27 - 0
commu/tests/test_post_repository.py

@@ -0,0 +1,27 @@
1
+from commu.infrastructure.memory_post_repository import InMemoryPostRepository
2
+from commu.entities.post import Post
3
+
4
+def test_add_and_get_post():
5
+    repo = InMemoryPostRepository()
6
+    post = Post("user1", "content", images=["img.png"])
7
+    repo.add(post)
8
+    assert repo.get(post.id) == post
9
+
10
+def test_search():
11
+    repo = InMemoryPostRepository()
12
+    p1 = Post("u", "hello apple")
13
+    p2 = Post("u", "banana orange")
14
+    repo.add(p1)
15
+    repo.add(p2)
16
+    results = repo.search("apple")
17
+    assert len(results) == 1 and results[0].id == p1.id
18
+
19
+def test_rate_post():
20
+    repo = InMemoryPostRepository()
21
+    p = Post("u", "rate me")
22
+    repo.add(p)
23
+    repo.rate_post(p.id, "userA", 3)
24
+    repo.rate_post(p.id, "userB", 5)
25
+    # userA updates rating
26
+    repo.rate_post(p.id, "userA", 4)
27
+    assert p.ratings == [("userB", 5), ("userA", 4)] or p.ratings == [("userA", 4), ("userB", 5)]

+ 57 - 0
commu/tests/test_post_service.py

@@ -0,0 +1,57 @@
1
+import pytest
2
+from commu.usecases.post_service import PostService
3
+from commu.usecases.user_service import UserService
4
+from commu.infrastructure.memory_user_repository import InMemoryUserRepository
5
+from commu.infrastructure.memory_post_repository import InMemoryPostRepository
6
+from commu.entities.user import UserRole
7
+
8
+@pytest.fixture
9
+def setup_services():
10
+    user_repo = InMemoryUserRepository()
11
+    post_repo = InMemoryPostRepository()
12
+    user_svc = UserService(user_repo)
13
+    post_svc = PostService(post_repo, user_repo)
14
+    admin = user_svc.register("admin", UserRole.ADMIN)
15
+    user = user_svc.register("user", UserRole.USER)
16
+    guest = user_svc.register("guest", UserRole.GUEST)
17
+    return user_svc, post_svc, admin, user, guest
18
+
19
+def test_create_post(setup_services):
20
+    user_svc, post_svc, admin, user, guest = setup_services
21
+    post = post_svc.create_post(user.id, "My post", images=["a.jpg"])
22
+    assert post.text == "My post"
23
+    assert post.images == ["a.jpg"]
24
+
25
+def test_guest_cannot_create_post(setup_services):
26
+    user_svc, post_svc, admin, user, guest = setup_services
27
+    with pytest.raises(PermissionError):
28
+        post_svc.create_post(guest.id, "forbidden")
29
+
30
+def test_search_posts(setup_services):
31
+    user_svc, post_svc, admin, user, guest = setup_services
32
+    post1 = post_svc.create_post(user.id, "Hello world")
33
+    post2 = post_svc.create_post(user.id, "Another post")
34
+    results = post_svc.search_posts("hello")
35
+    assert len(results) == 1
36
+    assert results[0].text == "Hello world"
37
+
38
+def test_rate_post(setup_services):
39
+    user_svc, post_svc, admin, user, guest = setup_services
40
+    post = post_svc.create_post(user.id, "My post")
41
+    post_svc.rate_post(post.id, admin.id, 5)
42
+    post_svc.rate_post(post.id, user.id, 4)
43
+    assert abs(post.average_rating() - 4.5) < 1e-6
44
+
45
+def test_guest_cannot_rate(setup_services):
46
+    user_svc, post_svc, admin, user, guest = setup_services
47
+    post = post_svc.create_post(user.id, "Guest can't rate")
48
+    with pytest.raises(PermissionError):
49
+        post_svc.rate_post(post.id, guest.id, 5)
50
+
51
+def test_rating_bounds(setup_services):
52
+    user_svc, post_svc, admin, user, guest = setup_services
53
+    post = post_svc.create_post(user.id, "Test bounds")
54
+    with pytest.raises(ValueError):
55
+        post_svc.rate_post(post.id, admin.id, 0)
56
+    with pytest.raises(ValueError):
57
+        post_svc.rate_post(post.id, admin.id, 6)

+ 25 - 0
commu/tests/test_user_service.py

@@ -0,0 +1,25 @@
1
+import pytest
2
+from commu.usecases.user_service import UserService
3
+from commu.infrastructure.memory_user_repository import InMemoryUserRepository
4
+from commu.entities.user import UserRole
5
+
6
+def test_register_user():
7
+    repo = InMemoryUserRepository()
8
+    svc = UserService(repo)
9
+    user = svc.register("testuser")
10
+    assert user.username == "testuser"
11
+    assert user.role == UserRole.USER
12
+    assert repo.get(user.id) == user
13
+
14
+def test_register_admin():
15
+    repo = InMemoryUserRepository()
16
+    svc = UserService(repo)
17
+    user = svc.register("adminuser", UserRole.ADMIN)
18
+    assert user.role == UserRole.ADMIN
19
+
20
+def test_get_user():
21
+    repo = InMemoryUserRepository()
22
+    svc = UserService(repo)
23
+    user = svc.register("abc")
24
+    found = svc.get_user(user.id)
25
+    assert found.username == "abc"

+ 0 - 0
commu/usecases/__init__.py


BIN
commu/usecases/__pycache__/__init__.cpython-310.pyc


BIN
commu/usecases/__pycache__/__init__.cpython-311.pyc


BIN
commu/usecases/__pycache__/post_service.cpython-310.pyc


BIN
commu/usecases/__pycache__/post_service.cpython-311.pyc


BIN
commu/usecases/__pycache__/user_service.cpython-310.pyc


BIN
commu/usecases/__pycache__/user_service.cpython-311.pyc


+ 28 - 0
commu/usecases/post_service.py

@@ -0,0 +1,28 @@
1
+from commu.entities.post import Post
2
+from commu.entities.user import UserRole
3
+
4
+class PostService:
5
+    def __init__(self, post_repo, user_repo):
6
+        self.post_repo = post_repo
7
+        self.user_repo = user_repo
8
+
9
+    def create_post(self, author_id, text, images=None, videos=None):
10
+        user = self.user_repo.get(author_id)
11
+        print(f"user = {user}")
12
+        print(f"role = {user.role}")
13
+        if not user or user.role == UserRole.GUEST:
14
+            raise PermissionError("User does not have permission to create posts")
15
+        post = Post(author_id, text, images, videos)
16
+        self.post_repo.add(post)
17
+        return post
18
+
19
+    def search_posts(self, query):
20
+        return self.post_repo.search(query)
21
+
22
+    def rate_post(self, post_id, user_id, rating):
23
+        user = self.user_repo.get(user_id)
24
+        if not user or user.role == UserRole.GUEST:
25
+            raise PermissionError("Guests cannot rate posts")
26
+        if not (1 <= rating <= 5):
27
+            raise ValueError("Rating must be between 1 and 5")
28
+        self.post_repo.rate_post(post_id, user_id, rating)

+ 13 - 0
commu/usecases/user_service.py

@@ -0,0 +1,13 @@
1
+from commu.entities.user import User, UserRole
2
+
3
+class UserService:
4
+    def __init__(self, user_repo):
5
+        self.user_repo = user_repo
6
+
7
+    def register(self, username, role=UserRole.USER):
8
+        user = User(username, role)
9
+        self.user_repo.add(user)
10
+        return user
11
+
12
+    def get_user(self, user_id):
13
+        return self.user_repo.get(user_id)

+ 0 - 0
core/__init__.py


BIN
core/__pycache__/__init__.cpython-311.pyc


BIN
core/__pycache__/admin.cpython-311.pyc


BIN
core/__pycache__/apps.cpython-311.pyc


BIN
core/__pycache__/models.cpython-311.pyc


BIN
core/__pycache__/tests.cpython-311.pyc


BIN
core/__pycache__/urls.cpython-311.pyc


BIN
core/__pycache__/views.cpython-311.pyc


+ 3 - 0
core/admin.py

@@ -0,0 +1,3 @@
1
+from django.contrib import admin
2
+
3
+# Register your models here.

+ 6 - 0
core/apps.py

@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class CoreConfig(AppConfig):
5
+    default_auto_field = 'django.db.models.BigAutoField'
6
+    name = 'core'

+ 38 - 0
core/migrations/0001_initial.py

@@ -0,0 +1,38 @@
1
+# Generated by Django 5.2.4 on 2025-08-03 16:36
2
+
3
+import django.db.models.deletion
4
+from django.conf import settings
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='Post',
19
+            fields=[
20
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('text', models.TextField()),
22
+                ('images', models.JSONField(blank=True, default=list)),
23
+                ('videos', models.JSONField(blank=True, default=list)),
24
+                ('created', models.DateTimeField(auto_now_add=True)),
25
+                ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
26
+            ],
27
+        ),
28
+        migrations.CreateModel(
29
+            name='PostRating',
30
+            fields=[
31
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
+                ('rating', models.IntegerField()),
33
+                ('created', models.DateTimeField(auto_now_add=True)),
34
+                ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='core.post')),
35
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
36
+            ],
37
+        ),
38
+    ]

+ 0 - 0
core/migrations/__init__.py


BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc


BIN
core/migrations/__pycache__/__init__.cpython-311.pyc


+ 18 - 0
core/models.py

@@ -0,0 +1,18 @@
1
+from django.db import models
2
+from django.contrib.auth import get_user_model
3
+
4
+User = get_user_model()
5
+
6
+class Post(models.Model):
7
+    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
8
+    text = models.TextField()
9
+    images = models.JSONField(default=list, blank=True)
10
+    videos = models.JSONField(default=list, blank=True)
11
+    created = models.DateTimeField(auto_now_add=True)
12
+
13
+class PostRating(models.Model):
14
+    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="ratings")
15
+    user = models.ForeignKey(User, on_delete=models.CASCADE)
16
+    rating = models.IntegerField()
17
+    created = models.DateTimeField(auto_now_add=True)
18
+

BIN
core/repositories/__pycache__/django_post_repository.cpython-311.pyc


BIN
core/repositories/__pycache__/django_user_repository.cpython-311.pyc


+ 112 - 0
core/repositories/django_post_repository.py

@@ -0,0 +1,112 @@
1
+from commu.interfaces.post_repository import PostRepository
2
+from core.models import Post, PostRating
3
+from django.contrib.auth import get_user_model
4
+
5
+User = get_user_model()
6
+
7
+class DjangoPostRepository(PostRepository):
8
+    def add(self, post):
9
+        # post: commu.entities.post.Post
10
+        try:
11
+            author = User.objects.get(id=post.author_id)
12
+        except User.DoesNotExist:
13
+            raise ValueError("Author not found")
14
+        obj = Post.objects.create(
15
+            author=author,
16
+            text=post.text,
17
+            images=post.images,
18
+            videos=post.videos,
19
+        )
20
+        post.id = str(obj.id)
21
+        return post
22
+
23
+    def get(self, post_id):
24
+        try:
25
+            obj = Post.objects.get(id=post_id)
26
+            from commu.entities.post import Post as DomainPost
27
+            domain_post = DomainPost(
28
+                author_id=str(obj.author.id),
29
+                text=obj.text,
30
+                images=obj.images,
31
+                videos=obj.videos,
32
+            )
33
+            domain_post.id = str(obj.id)
34
+            # Load ratings
35
+            domain_post.ratings = [
36
+                (str(r.user.id), r.rating)
37
+                for r in obj.ratings.all()
38
+            ]
39
+            return domain_post
40
+        except Post.DoesNotExist:
41
+            return None
42
+
43
+    def search(self, query: str):
44
+        qs = Post.objects.filter(text__icontains=query)
45
+        from commu.entities.post import Post as DomainPost
46
+        result = []
47
+        for obj in qs:
48
+            domain_post = DomainPost(
49
+                author_id=str(obj.author.id),
50
+                text=obj.text,
51
+                images=obj.images,
52
+                videos=obj.videos,
53
+            )
54
+            domain_post.id = str(obj.id)
55
+            # Load ratings
56
+            domain_post.ratings = [
57
+                (str(r.user.id), r.rating)
58
+                for r in obj.ratings.all()
59
+            ]
60
+            result.append(domain_post)
61
+        return result
62
+
63
+    def rate_post(self, post_id, user_id, rating: int):
64
+        try:
65
+            post = Post.objects.get(id=post_id)
66
+            user = User.objects.get(id=user_id)
67
+        except (Post.DoesNotExist, User.DoesNotExist):
68
+            raise ValueError("Post or user not found")
69
+        # Remove old rating from user, if any
70
+        PostRating.objects.filter(post=post, user=user).delete()
71
+        # Create new rating
72
+        PostRating.objects.create(post=post, user=user, rating=rating)
73
+
74
+    def delete(self, post_id):
75
+        try:
76
+            obj = Post.objects.get(id=post_id)
77
+            obj.delete()
78
+        except Post.DoesNotExist:
79
+            raise ValueError("Post not found")
80
+
81
+    def list(self, page=1, per_page=10, order_by="-created"):
82
+        qs = Post.objects.all().order_by(order_by)
83
+        total = qs.count()
84
+        start = (page - 1) * per_page
85
+        end = start + per_page
86
+        from commu.entities.post import Post as DomainPost
87
+        posts = []
88
+        for obj in qs[start:end]:
89
+            domain_post = DomainPost(
90
+                author_id=str(obj.author.id),
91
+                text=obj.text,
92
+                images=obj.images,
93
+                videos=obj.videos,
94
+            )
95
+            domain_post.id = str(obj.id)
96
+            domain_post.ratings = [
97
+                (str(r.user.id), r.rating)
98
+                for r in obj.ratings.all()
99
+            ]
100
+            posts.append(domain_post)
101
+        return posts, total
102
+
103
+    def update(self, post):
104
+        try:
105
+            obj = Post.objects.get(id=post.id)
106
+            obj.text = post.text
107
+            obj.images = post.images
108
+            obj.videos = post.videos
109
+            obj.save()
110
+            # Optionally update ratings (if part of update)
111
+        except Post.DoesNotExist:
112
+            raise ValueError("Post not found")

+ 55 - 0
core/repositories/django_user_repository.py

@@ -0,0 +1,55 @@
1
+from commu.interfaces.user_repository import UserRepository
2
+from django.contrib.auth.models import User as DjangoUser
3
+
4
+class DjangoUserRepository(UserRepository):
5
+    def add(self, user):
6
+        # user: commu.entities.user.User
7
+        # Map role to Django flags
8
+        is_staff = user.role.value in ("admin", "user") if hasattr(user.role, "value") else user.role in ("admin", "user")
9
+        is_superuser = user.role.value == "admin" if hasattr(user.role, "value") else user.role == "admin"
10
+        obj = DjangoUser.objects.create_user(
11
+            username=user.username,
12
+            password=None,  # You may set/generate a password as needed!
13
+            is_staff=is_staff,
14
+            is_superuser=is_superuser,
15
+        )
16
+        user.id = str(obj.id)
17
+        return user
18
+
19
+    def get(self, user_id):
20
+        try:
21
+            obj = DjangoUser.objects.get(id=user_id)
22
+            from commu.entities.user import User, UserRole
23
+            role = self._role_from_django(obj)
24
+            u = User(
25
+                username=obj.username,
26
+                role=role,
27
+            )
28
+            u.id = str(obj.id)
29
+            return u
30
+        except DjangoUser.DoesNotExist:
31
+            return None
32
+
33
+    def find_by_username(self, username):
34
+        try:
35
+            obj = DjangoUser.objects.get(username=username)
36
+            from commu.entities.user import User, UserRole
37
+            role = self._role_from_django(obj)
38
+            u = User(
39
+                username=obj.username,
40
+                role=role,
41
+            )
42
+            u.id = str(obj.id)
43
+            return u
44
+        except DjangoUser.DoesNotExist:
45
+            return None
46
+
47
+    def _role_from_django(self, user_obj):
48
+        from commu.entities.user import UserRole
49
+        if user_obj.is_superuser:
50
+            return UserRole.ADMIN
51
+        elif user_obj.is_staff:
52
+            return UserRole.USER
53
+        else:
54
+            return UserRole.GUEST
55
+

+ 3 - 0
core/tests.py

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 0 - 0
core/tests/__init__.py


BIN
core/tests/__pycache__/__init__.cpython-311.pyc


BIN
core/tests/__pycache__/test_post_service_django.cpython-311.pyc


+ 60 - 0
core/tests/test_post_service_django.py

@@ -0,0 +1,60 @@
1
+import pytest
2
+from django.test import TestCase
3
+from django.contrib.auth import get_user_model
4
+
5
+from commu.usecases.post_service import PostService
6
+from commu.entities.user import UserRole
7
+from core.repositories.django_post_repository import DjangoPostRepository
8
+from core.repositories.django_user_repository import DjangoUserRepository
9
+
10
+User = get_user_model()
11
+
12
+class PostServiceIntegrationTest(TestCase):
13
+
14
+    def setUp(self):
15
+        # Use Django's repositories
16
+        self.user_repo = DjangoUserRepository()
17
+        self.post_repo = DjangoPostRepository()
18
+        self.service = PostService(self.post_repo, self.user_repo)
19
+
20
+        # Create some users using Django ORM
21
+        self.admin = User.objects.create_user(username="admin", is_superuser=True, is_staff=True)
22
+        self.user = User.objects.create_user(username="user", is_staff=True)
23
+        self.guest = User.objects.create_user(username="guest")  # Not staff or superuser
24
+
25
+    def test_create_post_by_user(self):
26
+        post = self.service.create_post(str(self.user.id), "Hello Django!", ["img1.png"], ["vid1.mp4"])
27
+        assert post.text == "Hello Django!"
28
+        assert post.images == ["img1.png"]
29
+        assert post.videos == ["vid1.mp4"]
30
+
31
+    def test_guest_cannot_create_post(self):
32
+        with self.assertRaises(PermissionError):
33
+            self.service.create_post(str(self.guest.id), "Forbidden")
34
+
35
+    def test_search_posts(self):
36
+        p1 = self.service.create_post(str(self.user.id), "First Django post")
37
+        p2 = self.service.create_post(str(self.user.id), "Second hello world")
38
+        results = self.service.search_posts("hello")
39
+        assert len(results) == 1
40
+        assert results[0].text == "Second hello world"
41
+
42
+    def test_rate_post(self):
43
+        post = self.service.create_post(str(self.user.id), "Rate me Django!")
44
+        self.service.rate_post(post.id, str(self.admin.id), 5)
45
+        self.service.rate_post(post.id, str(self.user.id), 4)
46
+        updated_post = self.post_repo.get(post.id)
47
+        assert abs(updated_post.average_rating() - 4.5) < 1e-6
48
+
49
+    def test_guest_cannot_rate_post(self):
50
+        post = self.service.create_post(str(self.user.id), "No guest rate")
51
+        with self.assertRaises(PermissionError):
52
+            self.service.rate_post(post.id, str(self.guest.id), 2)
53
+
54
+    def test_rating_bounds(self):
55
+        post = self.service.create_post(str(self.user.id), "Bounds Django")
56
+        with self.assertRaises(ValueError):
57
+            self.service.rate_post(post.id, str(self.admin.id), 0)
58
+        with self.assertRaises(ValueError):
59
+            self.service.rate_post(post.id, str(self.admin.id), 6)
60
+

+ 8 - 0
core/urls.py

@@ -0,0 +1,8 @@
1
+from django.urls import path
2
+from . import views
3
+
4
+urlpatterns = [
5
+    path('posts/create/', views.create_post, name='create_post'),
6
+    path('posts/search/', views.search_posts, name='search_posts'),
7
+    path('posts/rate/', views.rate_post, name='rate_post'),
8
+]

+ 69 - 0
core/views.py

@@ -0,0 +1,69 @@
1
+import json
2
+from django.http import JsonResponse, HttpResponseBadRequest
3
+from django.views.decorators.csrf import csrf_exempt
4
+from django.contrib.auth.decorators import login_required
5
+from commu.usecases.post_service import PostService
6
+from core.repositories.django_post_repository import DjangoPostRepository
7
+from core.repositories.django_user_repository import DjangoUserRepository
8
+
9
+# Service setup (could be moved to app config for larger apps)
10
+user_repo = DjangoUserRepository()
11
+post_repo = DjangoPostRepository()
12
+post_service = PostService(post_repo, user_repo)
13
+
14
+@csrf_exempt
15
+@login_required
16
+def create_post(request):
17
+    if request.method != 'POST':
18
+        return HttpResponseBadRequest("POST only")
19
+    try:
20
+        data = json.loads(request.body.decode())
21
+        text = data.get("text")
22
+        images = data.get("images", [])
23
+        videos = data.get("videos", [])
24
+        if not text:
25
+            return JsonResponse({"error": "Missing post text"}, status=400)
26
+        # Use logged-in Django user as author
27
+        user_id = str(request.user.id)
28
+        post = post_service.create_post(user_id, text, images, videos)
29
+        return JsonResponse({
30
+            "id": post.id,
31
+            "text": post.text,
32
+            "author_id": post.author_id,
33
+            "images": post.images,
34
+            "videos": post.videos
35
+        })
36
+    except Exception as e:
37
+        return JsonResponse({"error": str(e)}, status=400)
38
+
39
+def search_posts(request):
40
+    q = request.GET.get("q", "")
41
+    results = post_service.search_posts(q)
42
+    posts = [{
43
+        "id": p.id,
44
+        "text": p.text,
45
+        "author_id": p.author_id,
46
+        "images": p.images,
47
+        "videos": p.videos,
48
+        "avg_rating": p.average_rating()
49
+    } for p in results]
50
+    return JsonResponse({"results": posts})
51
+
52
+@csrf_exempt
53
+@login_required
54
+def rate_post(request):
55
+    if request.method != 'POST':
56
+        return HttpResponseBadRequest("POST only")
57
+    try:
58
+        data = json.loads(request.body.decode())
59
+        post_id = data.get("post_id")
60
+        rating = data.get("rating")
61
+        if not post_id or rating is None:
62
+            return JsonResponse({"error": "Missing post_id or rating"}, status=400)
63
+        user_id = str(request.user.id)
64
+        post_service.rate_post(post_id, user_id, int(rating))
65
+        return JsonResponse({"ok": True})
66
+    except Exception as e:
67
+        return JsonResponse({"error": str(e)}, status=400)
68
+
69
+

+ 0 - 0
db.sqlite3


+ 0 - 0
django_commu/__init__.py


BIN
django_commu/__pycache__/__init__.cpython-311.pyc


BIN
django_commu/__pycache__/settings.cpython-311.pyc


BIN
django_commu/__pycache__/urls.cpython-311.pyc


BIN
django_commu/__pycache__/wsgi.cpython-311.pyc


+ 16 - 0
django_commu/asgi.py

@@ -0,0 +1,16 @@
1
+"""
2
+ASGI config for django_commu project.
3
+
4
+It exposes the ASGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.asgi import get_asgi_application
13
+
14
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_commu.settings')
15
+
16
+application = get_asgi_application()

+ 128 - 0
django_commu/settings.py

@@ -0,0 +1,128 @@
1
+"""
2
+Django settings for django_commu project.
3
+
4
+Generated by 'django-admin startproject' using Django 5.2.4.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/topics/settings/
8
+
9
+For the full list of settings and their values, see
10
+https://docs.djangoproject.com/en/5.2/ref/settings/
11
+"""
12
+
13
+from pathlib import Path
14
+import os
15
+
16
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
17
+BASE_DIR = Path(__file__).resolve().parent.parent
18
+
19
+
20
+# Quick-start development settings - unsuitable for production
21
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
22
+
23
+# SECURITY WARNING: keep the secret key used in production secret!
24
+SECRET_KEY = 'django-insecure-c$ieo22nwe^v#*e$xlh@fath9*t+f8wtqrt9e!op9-ittc=b_x'
25
+
26
+# SECURITY WARNING: don't run with debug turned on in production!
27
+DEBUG = True
28
+
29
+ALLOWED_HOSTS = []
30
+
31
+
32
+# Application definition
33
+
34
+INSTALLED_APPS = [
35
+    'django.contrib.admin',
36
+    'django.contrib.auth',
37
+    'django.contrib.contenttypes',
38
+    'django.contrib.sessions',
39
+    'django.contrib.messages',
40
+    'django.contrib.staticfiles',
41
+    'core',
42
+]
43
+
44
+MIDDLEWARE = [
45
+    'django.middleware.security.SecurityMiddleware',
46
+    'django.contrib.sessions.middleware.SessionMiddleware',
47
+    'django.middleware.common.CommonMiddleware',
48
+    'django.middleware.csrf.CsrfViewMiddleware',
49
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
50
+    'django.contrib.messages.middleware.MessageMiddleware',
51
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
52
+]
53
+
54
+ROOT_URLCONF = 'django_commu.urls'
55
+
56
+TEMPLATES = [
57
+    {
58
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
59
+        'DIRS': [],
60
+        'APP_DIRS': True,
61
+        'OPTIONS': {
62
+            'context_processors': [
63
+                'django.template.context_processors.request',
64
+                'django.contrib.auth.context_processors.auth',
65
+                'django.contrib.messages.context_processors.messages',
66
+            ],
67
+        },
68
+    },
69
+]
70
+
71
+WSGI_APPLICATION = 'django_commu.wsgi.application'
72
+
73
+
74
+# Database
75
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
76
+
77
+DATABASES = {
78
+    'default': {
79
+        'ENGINE': 'django.db.backends.postgresql',
80
+        'NAME': os.getenv('DB_NAME', 'commudb'),
81
+        'USER': os.getenv('DB_USER', 'commuuser'),
82
+        'PASSWORD': os.getenv('DB_PASSWORD', 'commupass'),
83
+        'HOST': os.getenv('DB_HOST', 'db'),
84
+        'PORT': os.getenv('DB_PORT', '5432'),
85
+    }
86
+}
87
+
88
+
89
+# Password validation
90
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
91
+
92
+AUTH_PASSWORD_VALIDATORS = [
93
+    {
94
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
95
+    },
96
+    {
97
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
98
+    },
99
+    {
100
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
101
+    },
102
+    {
103
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
104
+    },
105
+]
106
+
107
+
108
+# Internationalization
109
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
110
+
111
+LANGUAGE_CODE = 'en-us'
112
+
113
+TIME_ZONE = 'UTC'
114
+
115
+USE_I18N = True
116
+
117
+USE_TZ = True
118
+
119
+
120
+# Static files (CSS, JavaScript, Images)
121
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
122
+
123
+STATIC_URL = 'static/'
124
+
125
+# Default primary key field type
126
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
127
+
128
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

+ 23 - 0
django_commu/urls.py

@@ -0,0 +1,23 @@
1
+"""
2
+URL configuration for django_commu project.
3
+
4
+The `urlpatterns` list routes URLs to views. For more information please see:
5
+    https://docs.djangoproject.com/en/5.2/topics/http/urls/
6
+Examples:
7
+Function views
8
+    1. Add an import:  from my_app import views
9
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
10
+Class-based views
11
+    1. Add an import:  from other_app.views import Home
12
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
13
+Including another URLconf
14
+    1. Import the include() function: from django.urls import include, path
15
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
16
+"""
17
+from django.contrib import admin
18
+from django.urls import path, include
19
+
20
+urlpatterns = [
21
+    path('admin/', admin.site.urls),
22
+    path('core/', include('core.urls')),
23
+]

+ 16 - 0
django_commu/wsgi.py

@@ -0,0 +1,16 @@
1
+"""
2
+WSGI config for django_commu project.
3
+
4
+It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.wsgi import get_wsgi_application
13
+
14
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_commu.settings')
15
+
16
+application = get_wsgi_application()

+ 40 - 0
docker-compose.yml

@@ -0,0 +1,40 @@
1
+version: '3.9'
2
+
3
+services:
4
+  db:
5
+    image: postgres:16
6
+    environment:
7
+      POSTGRES_DB: commudb
8
+      POSTGRES_USER: commuuser
9
+      POSTGRES_PASSWORD: commupass
10
+    ports:
11
+      - "5432:5432"
12
+    volumes:
13
+      - postgres_data:/var/lib/postgresql/data
14
+
15
+  web:
16
+    build:
17
+      context: .
18
+      args:
19
+        UID: ${UID}
20
+        GID: ${GID}
21
+    user: "${UID}:${GID}"
22
+    command: >
23
+      sh -c "python manage.py runserver 0.0.0.0:8000"
24
+    volumes:
25
+      - .:/app
26
+    working_dir: /app
27
+    ports:
28
+      - "8000:8000"
29
+    depends_on:
30
+      - db
31
+    environment:
32
+      - DEBUG=1
33
+      - DB_NAME=commudb
34
+      - DB_USER=commuuser
35
+      - DB_PASSWORD=commupass
36
+      - DB_HOST=db
37
+      - DB_PORT=5432
38
+
39
+volumes:
40
+  postgres_data:

+ 38 - 0
main.py

@@ -0,0 +1,38 @@
1
+from commu.infrastructure.memory_user_repository import InMemoryUserRepository
2
+from commu.infrastructure.memory_post_repository import InMemoryPostRepository
3
+from commu.usecases.user_service import UserService
4
+from commu.usecases.post_service import PostService
5
+from commu.entities.user import UserRole
6
+
7
+def main():
8
+    user_repo = InMemoryUserRepository()
9
+    post_repo = InMemoryPostRepository()
10
+    user_service = UserService(user_repo)
11
+    post_service = PostService(post_repo, user_repo)
12
+
13
+    # Create users
14
+    admin = user_service.register("alice", UserRole.ADMIN)
15
+    user = user_service.register("bob", UserRole.USER)
16
+    guest = user_service.register("guest", UserRole.GUEST)
17
+
18
+    # User creates post
19
+    post = post_service.create_post(user.id, "Hello world!", images=["img1.png"], videos=["vid1.mp4"])
20
+    print(f"Created Post: {post.text}, by {user.username}")
21
+
22
+    # Post search
23
+    found = post_service.search_posts("hello")
24
+    print(f"Found {len(found)} posts containing 'hello'.")
25
+
26
+    # Post rating
27
+    post_service.rate_post(post.id, admin.id, 5)
28
+    post_service.rate_post(post.id, user.id, 4)
29
+    print(f"Average Rating: {post.average_rating()}")
30
+
31
+    # Guest tries to create post (should fail)
32
+    try:
33
+        post_service.create_post(guest.id, "I am a guest")
34
+    except PermissionError as e:
35
+        print("Guest cannot create post:", e)
36
+
37
+if __name__ == "__main__":
38
+    main()

+ 22 - 0
manage.py

@@ -0,0 +1,22 @@
1
+#!/usr/bin/env python
2
+"""Django's command-line utility for administrative tasks."""
3
+import os
4
+import sys
5
+
6
+
7
+def main():
8
+    """Run administrative tasks."""
9
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_commu.settings')
10
+    try:
11
+        from django.core.management import execute_from_command_line
12
+    except ImportError as exc:
13
+        raise ImportError(
14
+            "Couldn't import Django. Are you sure it's installed and "
15
+            "available on your PYTHONPATH environment variable? Did you "
16
+            "forget to activate a virtual environment?"
17
+        ) from exc
18
+    execute_from_command_line(sys.argv)
19
+
20
+
21
+if __name__ == '__main__':
22
+    main()

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
1
+pytest
2
+Django>=4.2
3
+psycopg2-binary

tum/whitesports - Gogs: Simplico Git Service

Geen omschrijving

theme.php 44KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175
  1. <?php
  2. /**
  3. * WordPress Theme Administration API
  4. *
  5. * @package WordPress
  6. * @subpackage Administration
  7. */
  8. /**
  9. * Remove a theme
  10. *
  11. * @since 2.8.0
  12. *
  13. * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
  14. *
  15. * @param string $stylesheet Stylesheet of the theme to delete.
  16. * @param string $redirect Redirect to page when complete.
  17. * @return bool|null|WP_Error True on success, false if `$stylesheet` is empty, WP_Error on failure.
  18. * Null if filesystem credentials are required to proceed.
  19. */
  20. function delete_theme( $stylesheet, $redirect = '' ) {
  21. global $wp_filesystem;
  22. if ( empty( $stylesheet ) ) {
  23. return false;
  24. }
  25. if ( empty( $redirect ) ) {
  26. $redirect = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );
  27. }
  28. ob_start();
  29. $credentials = request_filesystem_credentials( $redirect );
  30. $data = ob_get_clean();
  31. if ( false === $credentials ) {
  32. if ( ! empty( $data ) ) {
  33. require_once ABSPATH . 'wp-admin/admin-header.php';
  34. echo $data;
  35. require_once ABSPATH . 'wp-admin/admin-footer.php';
  36. exit;
  37. }
  38. return;
  39. }
  40. if ( ! WP_Filesystem( $credentials ) ) {
  41. ob_start();
  42. // Failed to connect. Error and request again.
  43. request_filesystem_credentials( $redirect, '', true );
  44. $data = ob_get_clean();
  45. if ( ! empty( $data ) ) {
  46. require_once ABSPATH . 'wp-admin/admin-header.php';
  47. echo $data;
  48. require_once ABSPATH . 'wp-admin/admin-footer.php';
  49. exit;
  50. }
  51. return;
  52. }
  53. if ( ! is_object( $wp_filesystem ) ) {
  54. return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
  55. }
  56. if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
  57. return new WP_Error( 'fs_error', __( 'Filesystem error.' ), $wp_filesystem->errors );
  58. }
  59. // Get the base plugin folder.
  60. $themes_dir = $wp_filesystem->wp_themes_dir();
  61. if ( empty( $themes_dir ) ) {
  62. return new WP_Error( 'fs_no_themes_dir', __( 'Unable to locate WordPress theme directory.' ) );
  63. }
  64. /**
  65. * Fires immediately before a theme deletion attempt.
  66. *
  67. * @since 5.8.0
  68. *
  69. * @param string $stylesheet Stylesheet of the theme to delete.
  70. */
  71. do_action( 'delete_theme', $stylesheet );
  72. $themes_dir = trailingslashit( $themes_dir );
  73. $theme_dir = trailingslashit( $themes_dir . $stylesheet );
  74. $deleted = $wp_filesystem->delete( $theme_dir, true );
  75. /**
  76. * Fires immediately after a theme deletion attempt.
  77. *
  78. * @since 5.8.0
  79. *
  80. * @param string $stylesheet Stylesheet of the theme to delete.
  81. * @param bool $deleted Whether the theme deletion was successful.
  82. */
  83. do_action( 'deleted_theme', $stylesheet, $deleted );
  84. if ( ! $deleted ) {
  85. return new WP_Error(
  86. 'could_not_remove_theme',
  87. /* translators: %s: Theme name. */
  88. sprintf( __( 'Could not fully remove the theme %s.' ), $stylesheet )
  89. );
  90. }
  91. $theme_translations = wp_get_installed_translations( 'themes' );
  92. // Remove language files, silently.
  93. if ( ! empty( $theme_translations[ $stylesheet ] ) ) {
  94. $translations = $theme_translations[ $stylesheet ];
  95. foreach ( $translations as $translation => $data ) {
  96. $wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.po' );
  97. $wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.mo' );
  98. $json_translation_files = glob( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '-*.json' );
  99. if ( $json_translation_files ) {
  100. array_map( array( $wp_filesystem, 'delete' ), $json_translation_files );
  101. }
  102. }
  103. }
  104. // Remove the theme from allowed themes on the network.
  105. if ( is_multisite() ) {
  106. WP_Theme::network_disable_theme( $stylesheet );
  107. }
  108. // Force refresh of theme update information.
  109. delete_site_transient( 'update_themes' );
  110. return true;
  111. }
  112. /**
  113. * Gets the page templates available in this theme.
  114. *
  115. * @since 1.5.0
  116. * @since 4.7.0 Added the `$post_type` parameter.
  117. *
  118. * @param WP_Post|null $post Optional. The post being edited, provided for context.
  119. * @param string $post_type Optional. Post type to get the templates for. Default 'page'.
  120. * @return string[] Array of template file names keyed by the template header name.
  121. */
  122. function get_page_templates( $post = null, $post_type = 'page' ) {
  123. return array_flip( wp_get_theme()->get_page_templates( $post, $post_type ) );
  124. }
  125. /**
  126. * Tidies a filename for url display by the theme editor.
  127. *
  128. * @since 2.9.0
  129. * @access private
  130. *
  131. * @param string $fullpath Full path to the theme file
  132. * @param string $containingfolder Path of the theme parent folder
  133. * @return string
  134. */
  135. function _get_template_edit_filename( $fullpath, $containingfolder ) {
  136. return str_replace( dirname( dirname( $containingfolder ) ), '', $fullpath );
  137. }
  138. /**
  139. * Check if there is an update for a theme available.
  140. *
  141. * Will display link, if there is an update available.
  142. *
  143. * @since 2.7.0
  144. *
  145. * @see get_theme_update_available()
  146. *
  147. * @param WP_Theme $theme Theme data object.
  148. */
  149. function theme_update_available( $theme ) {
  150. echo get_theme_update_available( $theme );
  151. }
  152. /**
  153. * Retrieve the update link if there is a theme update available.
  154. *
  155. * Will return a link if there is an update available.
  156. *
  157. * @since 3.8.0
  158. *
  159. * @param WP_Theme $theme WP_Theme object.
  160. * @return string|false HTML for the update link, or false if invalid info was passed.
  161. */
  162. function get_theme_update_available( $theme ) {
  163. static $themes_update = null;
  164. if ( ! current_user_can( 'update_themes' ) ) {
  165. return false;
  166. }
  167. if ( ! isset( $themes_update ) ) {
  168. $themes_update = get_site_transient( 'update_themes' );
  169. }
  170. if ( ! ( $theme instanceof WP_Theme ) ) {
  171. return false;
  172. }
  173. $stylesheet = $theme->get_stylesheet();
  174. $html = '';
  175. if ( isset( $themes_update->response[ $stylesheet ] ) ) {
  176. $update = $themes_update->response[ $stylesheet ];
  177. $theme_name = $theme->display( 'Name' );
  178. $details_url = add_query_arg(
  179. array(
  180. 'TB_iframe' => 'true',
  181. 'width' => 1024,
  182. 'height' => 800,
  183. ),
  184. $update['url']
  185. ); // Theme browser inside WP? Replace this. Also, theme preview JS will override this on the available list.
  186. $update_url = wp_nonce_url( admin_url( 'update.php?action=upgrade-theme&amp;theme=' . urlencode( $stylesheet ) ), 'upgrade-theme_' . $stylesheet );
  187. if ( ! is_multisite() ) {
  188. if ( ! current_user_can( 'update_themes' ) ) {
  189. $html = sprintf(
  190. /* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number. */
  191. '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>.' ) . '</strong></p>',
  192. $theme_name,
  193. esc_url( $details_url ),
  194. sprintf(
  195. 'class="thickbox open-plugin-details-modal" aria-label="%s"',
  196. /* translators: 1: Theme name, 2: Version number. */
  197. esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
  198. ),
  199. $update['new_version']
  200. );
  201. } elseif ( empty( $update['package'] ) ) {
  202. $html = sprintf(
  203. /* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number. */
  204. '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ) . '</strong></p>',
  205. $theme_name,
  206. esc_url( $details_url ),
  207. sprintf(
  208. 'class="thickbox open-plugin-details-modal" aria-label="%s"',
  209. /* translators: 1: Theme name, 2: Version number. */
  210. esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
  211. ),
  212. $update['new_version']
  213. );
  214. } else {
  215. $html = sprintf(
  216. /* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number, 5: Update URL, 6: Additional link attributes. */
  217. '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a> or <a href="%5$s" %6$s>update now</a>.' ) . '</strong></p>',
  218. $theme_name,
  219. esc_url( $details_url ),
  220. sprintf(
  221. 'class="thickbox open-plugin-details-modal" aria-label="%s"',
  222. /* translators: 1: Theme name, 2: Version number. */
  223. esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
  224. ),
  225. $update['new_version'],
  226. $update_url,
  227. sprintf(
  228. 'aria-label="%s" id="update-theme" data-slug="%s"',
  229. /* translators: %s: Theme name. */
  230. esc_attr( sprintf( _x( 'Update %s now', 'theme' ), $theme_name ) ),
  231. $stylesheet
  232. )
  233. );
  234. }
  235. }
  236. }
  237. return $html;
  238. }
  239. /**
  240. * Retrieve list of WordPress theme features (aka theme tags).
  241. *
  242. * @since 3.1.0
  243. * @since 3.2.0 Added 'Gray' color and 'Featured Image Header', 'Featured Images',
  244. * 'Full Width Template', and 'Post Formats' features.
  245. * @since 3.5.0 Added 'Flexible Header' feature.
  246. * @since 3.8.0 Renamed 'Width' filter to 'Layout'.
  247. * @since 3.8.0 Renamed 'Fixed Width' and 'Flexible Width' options
  248. * to 'Fixed Layout' and 'Fluid Layout'.
  249. * @since 3.8.0 Added 'Accessibility Ready' feature and 'Responsive Layout' option.
  250. * @since 3.9.0 Combined 'Layout' and 'Columns' filters.
  251. * @since 4.6.0 Removed 'Colors' filter.
  252. * @since 4.6.0 Added 'Grid Layout' option.
  253. * Removed 'Fixed Layout', 'Fluid Layout', and 'Responsive Layout' options.
  254. * @since 4.6.0 Added 'Custom Logo' and 'Footer Widgets' features.
  255. * Removed 'Blavatar' feature.
  256. * @since 4.6.0 Added 'Blog', 'E-Commerce', 'Education', 'Entertainment', 'Food & Drink',
  257. * 'Holiday', 'News', 'Photography', and 'Portfolio' subjects.
  258. * Removed 'Photoblogging' and 'Seasonal' subjects.
  259. * @since 4.9.0 Reordered the filters from 'Layout', 'Features', 'Subject'
  260. * to 'Subject', 'Features', 'Layout'.
  261. * @since 4.9.0 Removed 'BuddyPress', 'Custom Menu', 'Flexible Header',
  262. * 'Front Page Posting', 'Microformats', 'RTL Language Support',
  263. * 'Threaded Comments', and 'Translation Ready' features.
  264. * @since 5.5.0 Added 'Block Editor Patterns', 'Block Editor Styles',
  265. * and 'Full Site Editing' features.
  266. * @since 5.5.0 Added 'Wide Blocks' layout option.
  267. * @since 5.8.1 Added 'Template Editing' feature.
  268. *
  269. * @param bool $api Optional. Whether try to fetch tags from the WordPress.org API. Defaults to true.
  270. * @return array Array of features keyed by category with translations keyed by slug.
  271. */
  272. function get_theme_feature_list( $api = true ) {
  273. // Hard-coded list is used if API is not accessible.
  274. $features = array(
  275. __( 'Subject' ) => array(
  276. 'blog' => __( 'Blog' ),
  277. 'e-commerce' => __( 'E-Commerce' ),
  278. 'education' => __( 'Education' ),
  279. 'entertainment' => __( 'Entertainment' ),
  280. 'food-and-drink' => __( 'Food & Drink' ),
  281. 'holiday' => __( 'Holiday' ),
  282. 'news' => __( 'News' ),
  283. 'photography' => __( 'Photography' ),
  284. 'portfolio' => __( 'Portfolio' ),
  285. ),
  286. __( 'Features' ) => array(
  287. 'accessibility-ready' => __( 'Accessibility Ready' ),
  288. 'block-patterns' => __( 'Block Editor Patterns' ),
  289. 'block-styles' => __( 'Block Editor Styles' ),
  290. 'custom-background' => __( 'Custom Background' ),
  291. 'custom-colors' => __( 'Custom Colors' ),
  292. 'custom-header' => __( 'Custom Header' ),
  293. 'custom-logo' => __( 'Custom Logo' ),
  294. 'editor-style' => __( 'Editor Style' ),
  295. 'featured-image-header' => __( 'Featured Image Header' ),
  296. 'featured-images' => __( 'Featured Images' ),
  297. 'footer-widgets' => __( 'Footer Widgets' ),
  298. 'full-site-editing' => __( 'Full Site Editing' ),
  299. 'full-width-template' => __( 'Full Width Template' ),
  300. 'post-formats' => __( 'Post Formats' ),
  301. 'sticky-post' => __( 'Sticky Post' ),
  302. 'template-editing' => __( 'Template Editing' ),
  303. 'theme-options' => __( 'Theme Options' ),
  304. ),
  305. __( 'Layout' ) => array(
  306. 'grid-layout' => __( 'Grid Layout' ),
  307. 'one-column' => __( 'One Column' ),
  308. 'two-columns' => __( 'Two Columns' ),
  309. 'three-columns' => __( 'Three Columns' ),
  310. 'four-columns' => __( 'Four Columns' ),
  311. 'left-sidebar' => __( 'Left Sidebar' ),
  312. 'right-sidebar' => __( 'Right Sidebar' ),
  313. 'wide-blocks' => __( 'Wide Blocks' ),
  314. ),
  315. );
  316. if ( ! $api || ! current_user_can( 'install_themes' ) ) {
  317. return $features;
  318. }
  319. $feature_list = get_site_transient( 'wporg_theme_feature_list' );
  320. if ( ! $feature_list ) {
  321. set_site_transient( 'wporg_theme_feature_list', array(), 3 * HOUR_IN_SECONDS );
  322. }
  323. if ( ! $feature_list ) {
  324. $feature_list = themes_api( 'feature_list', array() );
  325. if ( is_wp_error( $feature_list ) ) {
  326. return $features;
  327. }
  328. }
  329. if ( ! $feature_list ) {
  330. return $features;
  331. }
  332. set_site_transient( 'wporg_theme_feature_list', $feature_list, 3 * HOUR_IN_SECONDS );
  333. $category_translations = array(
  334. 'Layout' => __( 'Layout' ),
  335. 'Features' => __( 'Features' ),
  336. 'Subject' => __( 'Subject' ),
  337. );
  338. $wporg_features = array();
  339. // Loop over the wp.org canonical list and apply translations.
  340. foreach ( (array) $feature_list as $feature_category => $feature_items ) {
  341. if ( isset( $category_translations[ $feature_category ] ) ) {
  342. $feature_category = $category_translations[ $feature_category ];
  343. }
  344. $wporg_features[ $feature_category ] = array();
  345. foreach ( $feature_items as $feature ) {
  346. if ( isset( $features[ $feature_category ][ $feature ] ) ) {
  347. $wporg_features[ $feature_category ][ $feature ] = $features[ $feature_category ][ $feature ];
  348. } else {
  349. $wporg_features[ $feature_category ][ $feature ] = $feature;
  350. }
  351. }
  352. }
  353. return $wporg_features;
  354. }
  355. /**
  356. * Retrieves theme installer pages from the WordPress.org Themes API.
  357. *
  358. * It is possible for a theme to override the Themes API result with three
  359. * filters. Assume this is for themes, which can extend on the Theme Info to
  360. * offer more choices. This is very powerful and must be used with care, when
  361. * overriding the filters.
  362. *
  363. * The first filter, {@see 'themes_api_args'}, is for the args and gives the action
  364. * as the second parameter. The hook for {@see 'themes_api_args'} must ensure that
  365. * an object is returned.
  366. *
  367. * The second filter, {@see 'themes_api'}, allows a plugin to override the WordPress.org
  368. * Theme API entirely. If `$action` is 'query_themes', 'theme_information', or 'feature_list',
  369. * an object MUST be passed. If `$action` is 'hot_tags', an array should be passed.
  370. *
  371. * Finally, the third filter, {@see 'themes_api_result'}, makes it possible to filter the
  372. * response object or array, depending on the `$action` type.
  373. *
  374. * Supported arguments per action:
  375. *
  376. * | Argument Name | 'query_themes' | 'theme_information' | 'hot_tags' | 'feature_list' |
  377. * | -------------------| :------------: | :-----------------: | :--------: | :--------------: |
  378. * | `$slug` | No | Yes | No | No |
  379. * | `$per_page` | Yes | No | No | No |
  380. * | `$page` | Yes | No | No | No |
  381. * | `$number` | No | No | Yes | No |
  382. * | `$search` | Yes | No | No | No |
  383. * | `$tag` | Yes | No | No | No |
  384. * | `$author` | Yes | No | No | No |
  385. * | `$user` | Yes | No | No | No |
  386. * | `$browse` | Yes | No | No | No |
  387. * | `$locale` | Yes | Yes | No | No |
  388. * | `$fields` | Yes | Yes | No | No |
  389. *
  390. * @since 2.8.0
  391. *
  392. * @param string $action API action to perform: 'query_themes', 'theme_information',
  393. * 'hot_tags' or 'feature_list'.
  394. * @param array|object $args {
  395. * Optional. Array or object of arguments to serialize for the Themes API.
  396. *
  397. * @type string $slug The theme slug. Default empty.
  398. * @type int $per_page Number of themes per page. Default 24.
  399. * @type int $page Number of current page. Default 1.
  400. * @type int $number Number of tags to be queried.
  401. * @type string $search A search term. Default empty.
  402. * @type string $tag Tag to filter themes. Default empty.
  403. * @type string $author Username of an author to filter themes. Default empty.
  404. * @type string $user Username to query for their favorites. Default empty.
  405. * @type string $browse Browse view: 'featured', 'popular', 'updated', 'favorites'.
  406. * @type string $locale Locale to provide context-sensitive results. Default is the value of get_locale().
  407. * @type array $fields {
  408. * Array of fields which should or should not be returned.
  409. *
  410. * @type bool $description Whether to return the theme full description. Default false.
  411. * @type bool $sections Whether to return the theme readme sections: description, installation,
  412. * FAQ, screenshots, other notes, and changelog. Default false.
  413. * @type bool $rating Whether to return the rating in percent and total number of ratings.
  414. * Default false.
  415. * @type bool $ratings Whether to return the number of rating for each star (1-5). Default false.
  416. * @type bool $downloaded Whether to return the download count. Default false.
  417. * @type bool $downloadlink Whether to return the download link for the package. Default false.
  418. * @type bool $last_updated Whether to return the date of the last update. Default false.
  419. * @type bool $tags Whether to return the assigned tags. Default false.
  420. * @type bool $homepage Whether to return the theme homepage link. Default false.
  421. * @type bool $screenshots Whether to return the screenshots. Default false.
  422. * @type int $screenshot_count Number of screenshots to return. Default 1.
  423. * @type bool $screenshot_url Whether to return the URL of the first screenshot. Default false.
  424. * @type bool $photon_screenshots Whether to return the screenshots via Photon. Default false.
  425. * @type bool $template Whether to return the slug of the parent theme. Default false.
  426. * @type bool $parent Whether to return the slug, name and homepage of the parent theme. Default false.
  427. * @type bool $versions Whether to return the list of all available versions. Default false.
  428. * @type bool $theme_url Whether to return theme's URL. Default false.
  429. * @type bool $extended_author Whether to return nicename or nicename and display name. Default false.
  430. * }
  431. * }
  432. * @return object|array|WP_Error Response object or array on success, WP_Error on failure. See the
  433. * {@link https://developer.wordpress.org/reference/functions/themes_api/ function reference article}
  434. * for more information on the make-up of possible return objects depending on the value of `$action`.
  435. */
  436. function themes_api( $action, $args = array() ) {
  437. // Include an unmodified $wp_version.
  438. require ABSPATH . WPINC . '/version.php';
  439. if ( is_array( $args ) ) {
  440. $args = (object) $args;
  441. }
  442. if ( 'query_themes' === $action ) {
  443. if ( ! isset( $args->per_page ) ) {
  444. $args->per_page = 24;
  445. }
  446. }
  447. if ( ! isset( $args->locale ) ) {
  448. $args->locale = get_user_locale();
  449. }
  450. if ( ! isset( $args->wp_version ) ) {
  451. $args->wp_version = substr( $wp_version, 0, 3 ); // x.y
  452. }
  453. /**
  454. * Filters arguments used to query for installer pages from the WordPress.org Themes API.
  455. *
  456. * Important: An object MUST be returned to this filter.
  457. *
  458. * @since 2.8.0
  459. *
  460. * @param object $args Arguments used to query for installer pages from the WordPress.org Themes API.
  461. * @param string $action Requested action. Likely values are 'theme_information',
  462. * 'feature_list', or 'query_themes'.
  463. */
  464. $args = apply_filters( 'themes_api_args', $args, $action );
  465. /**
  466. * Filters whether to override the WordPress.org Themes API.
  467. *
  468. * Passing a non-false value will effectively short-circuit the WordPress.org API request.
  469. *
  470. * If `$action` is 'query_themes', 'theme_information', or 'feature_list', an object MUST
  471. * be passed. If `$action` is 'hot_tags', an array should be passed.
  472. *
  473. * @since 2.8.0
  474. *
  475. * @param false|object|array $override Whether to override the WordPress.org Themes API. Default false.
  476. * @param string $action Requested action. Likely values are 'theme_information',
  477. * 'feature_list', or 'query_themes'.
  478. * @param object $args Arguments used to query for installer pages from the Themes API.
  479. */
  480. $res = apply_filters( 'themes_api', false, $action, $args );
  481. if ( ! $res ) {
  482. $url = 'http://api.wordpress.org/themes/info/1.2/';
  483. $url = add_query_arg(
  484. array(
  485. 'action' => $action,
  486. 'request' => $args,
  487. ),
  488. $url
  489. );
  490. $http_url = $url;
  491. $ssl = wp_http_supports( array( 'ssl' ) );
  492. if ( $ssl ) {
  493. $url = set_url_scheme( $url, 'https' );
  494. }
  495. $http_args = array(
  496. 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ),
  497. );
  498. $request = wp_remote_get( $url, $http_args );
  499. if ( $ssl && is_wp_error( $request ) ) {
  500. if ( ! wp_doing_ajax() ) {
  501. trigger_error(
  502. sprintf(
  503. /* translators: %s: Support forums URL. */
  504. __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
  505. __( 'https://wordpress.org/support/forums/' )
  506. ) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
  507. headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
  508. );
  509. }
  510. $request = wp_remote_get( $http_url, $http_args );
  511. }
  512. if ( is_wp_error( $request ) ) {
  513. $res = new WP_Error(
  514. 'themes_api_failed',
  515. sprintf(
  516. /* translators: %s: Support forums URL. */
  517. __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
  518. __( 'https://wordpress.org/support/forums/' )
  519. ),
  520. $request->get_error_message()
  521. );
  522. } else {
  523. $res = json_decode( wp_remote_retrieve_body( $request ), true );
  524. if ( is_array( $res ) ) {
  525. // Object casting is required in order to match the info/1.0 format.
  526. $res = (object) $res;
  527. } elseif ( null === $res ) {
  528. $res = new WP_Error(
  529. 'themes_api_failed',
  530. sprintf(
  531. /* translators: %s: Support forums URL. */
  532. __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
  533. __( 'https://wordpress.org/support/forums/' )
  534. ),
  535. wp_remote_retrieve_body( $request )
  536. );
  537. }
  538. if ( isset( $res->error ) ) {
  539. $res = new WP_Error( 'themes_api_failed', $res->error );
  540. }
  541. }
  542. // Back-compat for info/1.2 API, upgrade the theme objects in query_themes to objects.
  543. if ( 'query_themes' === $action ) {
  544. foreach ( $res->themes as $i => $theme ) {
  545. $res->themes[ $i ] = (object) $theme;
  546. }
  547. }
  548. // Back-compat for info/1.2 API, downgrade the feature_list result back to an array.
  549. if ( 'feature_list' === $action ) {
  550. $res = (array) $res;
  551. }
  552. }
  553. /**
  554. * Filters the returned WordPress.org Themes API response.
  555. *
  556. * @since 2.8.0
  557. *
  558. * @param array|object|WP_Error $res WordPress.org Themes API response.
  559. * @param string $action Requested action. Likely values are 'theme_information',
  560. * 'feature_list', or 'query_themes'.
  561. * @param object $args Arguments used to query for installer pages from the WordPress.org Themes API.
  562. */
  563. return apply_filters( 'themes_api_result', $res, $action, $args );
  564. }
  565. /**
  566. * Prepare themes for JavaScript.
  567. *
  568. * @since 3.8.0
  569. *
  570. * @param WP_Theme[] $themes Optional. Array of theme objects to prepare.
  571. * Defaults to all allowed themes.
  572. *
  573. * @return array An associative array of theme data, sorted by name.
  574. */
  575. function wp_prepare_themes_for_js( $themes = null ) {
  576. $current_theme = get_stylesheet();
  577. /**
  578. * Filters theme data before it is prepared for JavaScript.
  579. *
  580. * Passing a non-empty array will result in wp_prepare_themes_for_js() returning
  581. * early with that value instead.
  582. *
  583. * @since 4.2.0
  584. *
  585. * @param array $prepared_themes An associative array of theme data. Default empty array.
  586. * @param WP_Theme[]|null $themes An array of theme objects to prepare, if any.
  587. * @param string $current_theme The current theme slug.
  588. */
  589. $prepared_themes = (array) apply_filters( 'pre_prepare_themes_for_js', array(), $themes, $current_theme );
  590. if ( ! empty( $prepared_themes ) ) {
  591. return $prepared_themes;
  592. }
  593. // Make sure the current theme is listed first.
  594. $prepared_themes[ $current_theme ] = array();
  595. if ( null === $themes ) {
  596. $themes = wp_get_themes( array( 'allowed' => true ) );
  597. if ( ! isset( $themes[ $current_theme ] ) ) {
  598. $themes[ $current_theme ] = wp_get_theme();
  599. }
  600. }
  601. $updates = array();
  602. $no_updates = array();
  603. if ( ! is_multisite() && current_user_can( 'update_themes' ) ) {
  604. $updates_transient = get_site_transient( 'update_themes' );
  605. if ( isset( $updates_transient->response ) ) {
  606. $updates = $updates_transient->response;
  607. }
  608. if ( isset( $updates_transient->no_update ) ) {
  609. $no_updates = $updates_transient->no_update;
  610. }
  611. }
  612. WP_Theme::sort_by_name( $themes );
  613. $parents = array();
  614. $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
  615. foreach ( $themes as $theme ) {
  616. $slug = $theme->get_stylesheet();
  617. $encoded_slug = urlencode( $slug );
  618. $parent = false;
  619. if ( $theme->parent() ) {
  620. $parent = $theme->parent();
  621. $parents[ $slug ] = $parent->get_stylesheet();
  622. $parent = $parent->display( 'Name' );
  623. }
  624. $customize_action = null;
  625. if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
  626. $customize_action = esc_url(
  627. add_query_arg(
  628. array(
  629. 'return' => urlencode( esc_url_raw( remove_query_arg( wp_removable_query_args(), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ),
  630. ),
  631. wp_customize_url( $slug )
  632. )
  633. );
  634. }
  635. $update_requires_wp = isset( $updates[ $slug ]['requires'] ) ? $updates[ $slug ]['requires'] : null;
  636. $update_requires_php = isset( $updates[ $slug ]['requires_php'] ) ? $updates[ $slug ]['requires_php'] : null;
  637. $auto_update = in_array( $slug, $auto_updates, true );
  638. $auto_update_action = $auto_update ? 'disable-auto-update' : 'enable-auto-update';
  639. if ( isset( $updates[ $slug ] ) ) {
  640. $auto_update_supported = true;
  641. $auto_update_filter_payload = (object) $updates[ $slug ];
  642. } elseif ( isset( $no_updates[ $slug ] ) ) {
  643. $auto_update_supported = true;
  644. $auto_update_filter_payload = (object) $no_updates[ $slug ];
  645. } else {
  646. $auto_update_supported = false;
  647. /*
  648. * Create the expected payload for the auto_update_theme filter, this is the same data
  649. * as contained within $updates or $no_updates but used when the Theme is not known.
  650. */
  651. $auto_update_filter_payload = (object) array(
  652. 'theme' => $slug,
  653. 'new_version' => $theme->get( 'Version' ),
  654. 'url' => '',
  655. 'package' => '',
  656. 'requires' => $theme->get( 'RequiresWP' ),
  657. 'requires_php' => $theme->get( 'RequiresPHP' ),
  658. );
  659. }
  660. $auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, $auto_update_filter_payload );
  661. $prepared_themes[ $slug ] = array(
  662. 'id' => $slug,
  663. 'name' => $theme->display( 'Name' ),
  664. 'screenshot' => array( $theme->get_screenshot() ), // @todo Multiple screenshots.
  665. 'description' => $theme->display( 'Description' ),
  666. 'author' => $theme->display( 'Author', false, true ),
  667. 'authorAndUri' => $theme->display( 'Author' ),
  668. 'tags' => $theme->display( 'Tags' ),
  669. 'version' => $theme->get( 'Version' ),
  670. 'compatibleWP' => is_wp_version_compatible( $theme->get( 'RequiresWP' ) ),
  671. 'compatiblePHP' => is_php_version_compatible( $theme->get( 'RequiresPHP' ) ),
  672. 'updateResponse' => array(
  673. 'compatibleWP' => is_wp_version_compatible( $update_requires_wp ),
  674. 'compatiblePHP' => is_php_version_compatible( $update_requires_php ),
  675. ),
  676. 'parent' => $parent,
  677. 'active' => $slug === $current_theme,
  678. 'hasUpdate' => isset( $updates[ $slug ] ),
  679. 'hasPackage' => isset( $updates[ $slug ] ) && ! empty( $updates[ $slug ]['package'] ),
  680. 'update' => get_theme_update_available( $theme ),
  681. 'autoupdate' => array(
  682. 'enabled' => $auto_update || $auto_update_forced,
  683. 'supported' => $auto_update_supported,
  684. 'forced' => $auto_update_forced,
  685. ),
  686. 'actions' => array(
  687. 'activate' => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&amp;stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null,
  688. 'customize' => $customize_action,
  689. 'delete' => ( ! is_multisite() && current_user_can( 'delete_themes' ) ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&amp;stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null,
  690. 'autoupdate' => wp_is_auto_update_enabled_for_type( 'theme' ) && ! is_multisite() && current_user_can( 'update_themes' )
  691. ? wp_nonce_url( admin_url( 'themes.php?action=' . $auto_update_action . '&amp;stylesheet=' . $encoded_slug ), 'updates' )
  692. : null,
  693. ),
  694. );
  695. }
  696. // Remove 'delete' action if theme has an active child.
  697. if ( ! empty( $parents ) && array_key_exists( $current_theme, $parents ) ) {
  698. unset( $prepared_themes[ $parents[ $current_theme ] ]['actions']['delete'] );
  699. }
  700. /**
  701. * Filters the themes prepared for JavaScript, for themes.php.
  702. *
  703. * Could be useful for changing the order, which is by name by default.
  704. *
  705. * @since 3.8.0
  706. *
  707. * @param array $prepared_themes Array of theme data.
  708. */
  709. $prepared_themes = apply_filters( 'wp_prepare_themes_for_js', $prepared_themes );
  710. $prepared_themes = array_values( $prepared_themes );
  711. return array_filter( $prepared_themes );
  712. }
  713. /**
  714. * Print JS templates for the theme-browsing UI in the Customizer.
  715. *
  716. * @since 4.2.0
  717. */
  718. function customize_themes_print_templates() {
  719. ?>
  720. <script type="text/html" id="tmpl-customize-themes-details-view">
  721. <div class="theme-backdrop"></div>
  722. <div class="theme-wrap wp-clearfix" role="document">
  723. <div class="theme-header">
  724. <button type="button" class="left dashicons dashicons-no"><span class="screen-reader-text"><?php _e( 'Show previous theme' ); ?></span></button>
  725. <button type="button" class="right dashicons dashicons-no"><span class="screen-reader-text"><?php _e( 'Show next theme' ); ?></span></button>
  726. <button type="button" class="close dashicons dashicons-no"><span class="screen-reader-text"><?php _e( 'Close details dialog' ); ?></span></button>
  727. </div>
  728. <div class="theme-about wp-clearfix">
  729. <div class="theme-screenshots">
  730. <# if ( data.screenshot && data.screenshot[0] ) { #>
  731. <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
  732. <# } else { #>
  733. <div class="screenshot blank"></div>
  734. <# } #>
  735. </div>
  736. <div class="theme-info">
  737. <# if ( data.active ) { #>
  738. <span class="current-label"><?php _e( 'Current Theme' ); ?></span>
  739. <# } #>
  740. <h2 class="theme-name">{{{ data.name }}}<span class="theme-version">
  741. <?php
  742. /* translators: %s: Theme version. */
  743. printf( __( 'Version: %s' ), '{{ data.version }}' );
  744. ?>
  745. </span></h2>
  746. <h3 class="theme-author">
  747. <?php
  748. /* translators: %s: Theme author link. */
  749. printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' );
  750. ?>
  751. </h3>
  752. <# if ( data.stars && 0 != data.num_ratings ) { #>
  753. <div class="theme-rating">
  754. {{{ data.stars }}}
  755. <a class="num-ratings" target="_blank" href="{{ data.reviews_url }}">
  756. <?php
  757. printf(
  758. '%1$s <span class="screen-reader-text">%2$s</span>',
  759. /* translators: %s: Number of ratings. */
  760. sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ),
  761. /* translators: Accessibility text. */
  762. __( '(opens in a new tab)' )
  763. );
  764. ?>
  765. </a>
  766. </div>
  767. <# } #>
  768. <# if ( data.hasUpdate ) { #>
  769. <# if ( data.updateResponse.compatibleWP && data.updateResponse.compatiblePHP ) { #>
  770. <div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}">
  771. <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
  772. {{{ data.update }}}
  773. </div>
  774. <# } else { #>
  775. <div class="notice notice-error notice-alt notice-large" data-slug="{{ data.id }}">
  776. <h3 class="notice-title"><?php _e( 'Update Incompatible' ); ?></h3>
  777. <p>
  778. <# if ( ! data.updateResponse.compatibleWP && ! data.updateResponse.compatiblePHP ) { #>
  779. <?php
  780. printf(
  781. /* translators: %s: Theme name. */
  782. __( 'There is a new version of %s available, but it doesn&#8217;t work with your versions of WordPress and PHP.' ),
  783. '{{{ data.name }}}'
  784. );
  785. if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
  786. printf(
  787. /* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
  788. ' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
  789. self_admin_url( 'update-core.php' ),
  790. esc_url( wp_get_update_php_url() )
  791. );
  792. wp_update_php_annotation( '</p><p><em>', '</em>' );
  793. } elseif ( current_user_can( 'update_core' ) ) {
  794. printf(
  795. /* translators: %s: URL to WordPress Updates screen. */
  796. ' ' . __( '<a href="%s">Please update WordPress</a>.' ),
  797. self_admin_url( 'update-core.php' )
  798. );
  799. } elseif ( current_user_can( 'update_php' ) ) {
  800. printf(
  801. /* translators: %s: URL to Update PHP page. */
  802. ' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
  803. esc_url( wp_get_update_php_url() )
  804. );
  805. wp_update_php_annotation( '</p><p><em>', '</em>' );
  806. }
  807. ?>
  808. <# } else if ( ! data.updateResponse.compatibleWP ) { #>
  809. <?php
  810. printf(
  811. /* translators: %s: Theme name. */
  812. __( 'There is a new version of %s available, but it doesn&#8217;t work with your version of WordPress.' ),
  813. '{{{ data.name }}}'
  814. );
  815. if ( current_user_can( 'update_core' ) ) {
  816. printf(
  817. /* translators: %s: URL to WordPress Updates screen. */
  818. ' ' . __( '<a href="%s">Please update WordPress</a>.' ),
  819. self_admin_url( 'update-core.php' )
  820. );
  821. }
  822. ?>
  823. <# } else if ( ! data.updateResponse.compatiblePHP ) { #>
  824. <?php
  825. printf(
  826. /* translators: %s: Theme name. */
  827. __( 'There is a new version of %s available, but it doesn&#8217;t work with your version of PHP.' ),
  828. '{{{ data.name }}}'
  829. );
  830. if ( current_user_can( 'update_php' ) ) {
  831. printf(
  832. /* translators: %s: URL to Update PHP page. */
  833. ' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
  834. esc_url( wp_get_update_php_url() )
  835. );
  836. wp_update_php_annotation( '</p><p><em>', '</em>' );
  837. }
  838. ?>
  839. <# } #>
  840. </p>
  841. </div>
  842. <# } #>
  843. <# } #>
  844. <# if ( data.parent ) { #>
  845. <p class="parent-theme">
  846. <?php
  847. printf(
  848. /* translators: %s: Theme name. */
  849. __( 'This is a child theme of %s.' ),
  850. '<strong>{{{ data.parent }}}</strong>'
  851. );
  852. ?>
  853. </p>
  854. <# } #>
  855. <# if ( ! data.compatibleWP || ! data.compatiblePHP ) { #>
  856. <div class="notice notice-error notice-alt notice-large"><p>
  857. <# if ( ! data.compatibleWP && ! data.compatiblePHP ) { #>
  858. <?php
  859. _e( 'This theme doesn&#8217;t work with your versions of WordPress and PHP.' );
  860. if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
  861. printf(
  862. /* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
  863. ' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
  864. self_admin_url( 'update-core.php' ),
  865. esc_url( wp_get_update_php_url() )
  866. );
  867. wp_update_php_annotation( '</p><p><em>', '</em>' );
  868. } elseif ( current_user_can( 'update_core' ) ) {
  869. printf(
  870. /* translators: %s: URL to WordPress Updates screen. */
  871. ' ' . __( '<a href="%s">Please update WordPress</a>.' ),
  872. self_admin_url( 'update-core.php' )
  873. );
  874. } elseif ( current_user_can( 'update_php' ) ) {
  875. printf(
  876. /* translators: %s: URL to Update PHP page. */
  877. ' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
  878. esc_url( wp_get_update_php_url() )
  879. );
  880. wp_update_php_annotation( '</p><p><em>', '</em>' );
  881. }
  882. ?>
  883. <# } else if ( ! data.compatibleWP ) { #>
  884. <?php
  885. _e( 'This theme doesn&#8217;t work with your version of WordPress.' );
  886. if ( current_user_can( 'update_core' ) ) {
  887. printf(
  888. /* translators: %s: URL to WordPress Updates screen. */
  889. ' ' . __( '<a href="%s">Please update WordPress</a>.' ),
  890. self_admin_url( 'update-core.php' )
  891. );
  892. }
  893. ?>
  894. <# } else if ( ! data.compatiblePHP ) { #>
  895. <?php
  896. _e( 'This theme doesn&#8217;t work with your version of PHP.' );
  897. if ( current_user_can( 'update_php' ) ) {
  898. printf(
  899. /* translators: %s: URL to Update PHP page. */
  900. ' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
  901. esc_url( wp_get_update_php_url() )
  902. );
  903. wp_update_php_annotation( '</p><p><em>', '</em>' );
  904. }
  905. ?>
  906. <# } #>
  907. </p></div>
  908. <# } #>
  909. <p class="theme-description">{{{ data.description }}}</p>
  910. <# if ( data.tags ) { #>
  911. <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
  912. <# } #>
  913. </div>
  914. </div>
  915. <div class="theme-actions">
  916. <# if ( data.active ) { #>
  917. <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></button>
  918. <# } else if ( 'installed' === data.type ) { #>
  919. <?php if ( current_user_can( 'delete_themes' ) ) { ?>
  920. <# if ( data.actions && data.actions['delete'] ) { #>
  921. <a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a>
  922. <# } #>
  923. <?php } ?>
  924. <# if ( data.compatibleWP && data.compatiblePHP ) { #>
  925. <button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></button>
  926. <# } else { #>
  927. <button class="button button-primary disabled"><?php _e( 'Live Preview' ); ?></button>
  928. <# } #>
  929. <# } else { #>
  930. <# if ( data.compatibleWP && data.compatiblePHP ) { #>
  931. <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
  932. <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install &amp; Preview' ); ?></button>
  933. <# } else { #>
  934. <button type="button" class="button disabled"><?php _ex( 'Cannot Install', 'theme' ); ?></button>
  935. <button type="button" class="button button-primary disabled"><?php _e( 'Install &amp; Preview' ); ?></button>
  936. <# } #>
  937. <# } #>
  938. </div>
  939. </div>
  940. </script>
  941. <?php
  942. }
  943. /**
  944. * Determines whether a theme is technically active but was paused while
  945. * loading.
  946. *
  947. * For more information on this and similar theme functions, check out
  948. * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
  949. * Conditional Tags} article in the Theme Developer Handbook.
  950. *
  951. * @since 5.2.0
  952. *
  953. * @param string $theme Path to the theme directory relative to the themes directory.
  954. * @return bool True, if in the list of paused themes. False, not in the list.
  955. */
  956. function is_theme_paused( $theme ) {
  957. if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
  958. return false;
  959. }
  960. if ( get_stylesheet() !== $theme && get_template() !== $theme ) {
  961. return false;
  962. }
  963. return array_key_exists( $theme, $GLOBALS['_paused_themes'] );
  964. }
  965. /**
  966. * Gets the error that was recorded for a paused theme.
  967. *
  968. * @since 5.2.0
  969. *
  970. * @param string $theme Path to the theme directory relative to the themes
  971. * directory.
  972. * @return array|false Array of error information as it was returned by
  973. * `error_get_last()`, or false if none was recorded.
  974. */
  975. function wp_get_theme_error( $theme ) {
  976. if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
  977. return false;
  978. }
  979. if ( ! array_key_exists( $theme, $GLOBALS['_paused_themes'] ) ) {
  980. return false;
  981. }
  982. return $GLOBALS['_paused_themes'][ $theme ];
  983. }
  984. /**
  985. * Tries to resume a single theme.
  986. *
  987. * If a redirect was provided and a functions.php file was found, we first ensure that
  988. * functions.php file does not throw fatal errors anymore.
  989. *
  990. * The way it works is by setting the redirection to the error before trying to
  991. * include the file. If the theme fails, then the redirection will not be overwritten
  992. * with the success message and the theme will not be resumed.
  993. *
  994. * @since 5.2.0
  995. *
  996. * @param string $theme Single theme to resume.
  997. * @param string $redirect Optional. URL to redirect to. Default empty string.
  998. * @return bool|WP_Error True on success, false if `$theme` was not paused,
  999. * `WP_Error` on failure.
  1000. */
  1001. function resume_theme( $theme, $redirect = '' ) {
  1002. list( $extension ) = explode( '/', $theme );
  1003. /*
  1004. * We'll override this later if the theme could be resumed without
  1005. * creating a fatal error.
  1006. */
  1007. if ( ! empty( $redirect ) ) {
  1008. $functions_path = '';
  1009. if ( strpos( STYLESHEETPATH, $extension ) ) {
  1010. $functions_path = STYLESHEETPATH . '/functions.php';
  1011. } elseif ( strpos( TEMPLATEPATH, $extension ) ) {
  1012. $functions_path = TEMPLATEPATH . '/functions.php';
  1013. }
  1014. if ( ! empty( $functions_path ) ) {
  1015. wp_redirect(
  1016. add_query_arg(
  1017. '_error_nonce',
  1018. wp_create_nonce( 'theme-resume-error_' . $theme ),
  1019. $redirect
  1020. )
  1021. );
  1022. // Load the theme's functions.php to test whether it throws a fatal error.
  1023. ob_start();
  1024. if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) {
  1025. define( 'WP_SANDBOX_SCRAPING', true );
  1026. }
  1027. include $functions_path;
  1028. ob_clean();
  1029. }
  1030. }
  1031. $result = wp_paused_themes()->delete( $extension );
  1032. if ( ! $result ) {
  1033. return new WP_Error(
  1034. 'could_not_resume_theme',
  1035. __( 'Could not resume the theme.' )
  1036. );
  1037. }
  1038. return true;
  1039. }
  1040. /**
  1041. * Renders an admin notice in case some themes have been paused due to errors.
  1042. *
  1043. * @since 5.2.0
  1044. *
  1045. * @global string $pagenow
  1046. */
  1047. function paused_themes_notice() {
  1048. if ( 'themes.php' === $GLOBALS['pagenow'] ) {
  1049. return;
  1050. }
  1051. if ( ! current_user_can( 'resume_themes' ) ) {
  1052. return;
  1053. }
  1054. if ( ! isset( $GLOBALS['_paused_themes'] ) || empty( $GLOBALS['_paused_themes'] ) ) {
  1055. return;
  1056. }
  1057. printf(
  1058. '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p><a href="%s">%s</a></p></div>',
  1059. __( 'One or more themes failed to load properly.' ),
  1060. __( 'You can find more details and make changes on the Themes screen.' ),
  1061. esc_url( admin_url( 'themes.php' ) ),
  1062. __( 'Go to the Themes screen' )
  1063. );
  1064. }