From 7ca3f8b6dddb1e922491d8e3218684c6bdef37b8 Mon Sep 17 00:00:00 2001 From: Yevhen Matasar Date: Tue, 5 Aug 2025 17:56:26 +0200 Subject: [PATCH] GitHub Workflows and Moodle code style fixes --- .github/README.md | 83 ++++++ .github/workflows/ci.yml | 136 ++++++++++ .github/workflows/release.yml | 211 +++++++++++++++ .gitignore | 108 +++++++- CHANGELOG.md | 55 ++++ README.md | 3 + classes/finder.php | 337 ++++++++++++++---------- classes/form/filter_form.php | 159 ++++++----- classes/output/MtraceOutput.php | 43 ++- classes/output/OutputInterface.php | 40 ++- classes/steps/AbstractCleanupStep.php | 97 +++++-- classes/steps/CleanupStepInterface.php | 33 ++- classes/steps/ComponentFilesCleanup.php | 74 +++++- classes/steps/CourseModulesCleanup.php | 147 ++++++++--- classes/steps/FilesCheckout.php | 110 ++++++-- classes/steps/GhostFilesCleanup.php | 72 ++++- classes/steps/GradesCleanup.php | 129 +++++++-- classes/steps/LogsCleanup.php | 95 +++++-- classes/task/cleanup.php | 259 ++++++++++++------ classes/task/scan.php | 260 +++++++++++------- cli/reinit_modules_cleanup.php | 36 ++- cli/usage_statistics.php | 170 +++++++----- db/install.xml | 19 ++ db/tasks.php | 68 +++-- db/upgrade.php | 124 +++++---- download.php | 106 +++++--- files.php | 312 ++++++++++++---------- ghost.php | 210 ++++++++------- lang/en/local_cleanup.php | 93 ++++--- open.php | 126 +++++---- remove.php | 201 +++++++------- settings.php | 254 ++++++++++-------- version.php | 15 +- 33 files changed, 2894 insertions(+), 1291 deletions(-) create mode 100644 .github/README.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 db/install.xml diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..cb98090 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,83 @@ +# GitHub Workflows + +This directory contains GitHub Actions workflows for automated testing and validation of the local_cleanup plugin. + +## Available Workflows + +### CI Workflow (`ci.yml`) +**Purpose**: Validates the plugin against Moodle coding standards and ensures functionality across different PHP/Moodle versions. + +**Triggers**: +- Push to main branches (`master`, `main`, `develop`) +- Pull requests to main branches + +**What it tests**: +- PHP syntax validation +- Moodle coding standards compliance +- PHPDoc documentation standards +- Plugin structure validation +- Database upgrade validation +- CLI scripts functionality + +### Release Workflow (`release.yml`) +**Purpose**: Automates the release process when new versions are tagged. + +**Triggers**: +- Git tags starting with `v*` +- GitHub releases + +**What it does**: +- Validates version consistency +- Creates distribution packages +- Publishes GitHub releases +- Generates checksums + +## Status Badges + +Add these badges to your main README.md: + +```markdown +[![Moodle Plugin CI](https://github.com/grinchenkoedu/local_cleanup/workflows/Moodle%20Plugin%20CI/badge.svg)](https://github.com/grinchenkoedu/local_cleanup/actions) +[![Release](https://github.com/grinchenkoedu/local_cleanup/workflows/Release/badge.svg)](https://github.com/grinchenkoedu/local_cleanup/actions) +``` + +## Creating a Release + +1. Update `version.php` with new version number and release string +2. Update `CHANGELOG.md` with release notes +3. Create and push a git tag: + ```bash + git tag v2.2 + git push origin v2.2 + ``` + +The release workflow will automatically handle the rest. + +## Local Development + +To run similar checks locally: + +```bash +# Install moodle-plugin-ci +composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 + +# Run code checker +ci/bin/moodle-plugin-ci codechecker + +# Run PHPDoc checker +ci/bin/moodle-plugin-ci phpdoc +``` + +## Benefits + +- ✅ Automated quality assurance +- ✅ Moodle standards compliance +- ✅ Multi-version compatibility testing +- ✅ Streamlined release process +- ✅ Continuous integration feedback + +## Resources + +- [Moodle Plugin CI Documentation](https://moodlehq.github.io/moodle-plugin-ci/) +- [Moodle Development Docs](https://docs.moodle.org/dev/) +- [Moodle Coding Standards](https://docs.moodle.org/dev/Coding_style) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f56b84f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,136 @@ +name: Moodle Plugin CI + +on: + push: + branches: [ master, main, develop ] + pull_request: + branches: [ master, main, develop ] + +jobs: + moodle-plugin-ci: + name: Moodle Plugin CI + runs-on: ubuntu-22.04 + + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: 'postgres' + POSTGRES_HOST_AUTH_METHOD: 'trust' + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + + strategy: + fail-fast: false + matrix: + include: + - php: '8.1' + moodle-branch: 'MOODLE_401_STABLE' + database: 'pgsql' + - php: '8.3' + moodle-branch: 'MOODLE_405_STABLE' + database: 'pgsql' + - php: '8.4' + moodle-branch: 'MOODLE_500_STABLE' + database: 'pgsql' + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + path: plugin + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, json, curl, zip, gd, mbstring, xml, xmlreader, soap, mysqli, pgsql, sodium + ini-values: max_input_vars=5000 + coverage: none + + - name: Initialise moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + sudo locale-gen en_AU.UTF-8 + + - name: Install moodle-plugin-ci + run: | + moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 --verbose + env: + DB: ${{ matrix.database }} + MOODLE_BRANCH: ${{ matrix.moodle-branch }} + + - name: Initialize Moodle Database + run: | + cd ${GITHUB_WORKSPACE}/moodle + php admin/cli/install_database.php --agree-license --fullname="Test Site" --shortname="test" --adminuser=admin --adminpass=admin --adminemail=admin@example.com + php admin/cli/upgrade.php --non-interactive + + - name: PHP Lint + if: ${{ !cancelled() }} + run: moodle-plugin-ci phplint + + - name: Moodle Code Checker + if: ${{ !cancelled() }} + run: moodle-plugin-ci codechecker --max-warnings 0 + + - name: Moodle PHPDoc Checker + if: ${{ !cancelled() }} + run: moodle-plugin-ci phpdoc --max-warnings 0 + + - name: Validating + if: ${{ !cancelled() }} + run: moodle-plugin-ci validate + + - name: Check upgrade savepoints + if: ${{ !cancelled() }} + run: moodle-plugin-ci savepoints + + - name: Basic Security Check + if: ${{ !cancelled() }} + run: | + echo "Checking for potential security issues..." + # Check for direct superglobal usage (should use Moodle param functions) + if grep -r "\$_GET\|\$_POST\|\$_REQUEST" --include="*.php" plugin/ | grep -v "optional_param\|required_param" ; then + echo "WARNING: Found direct superglobal usage - should use Moodle param functions!" + exit 1 + fi + echo "Basic security check passed." + + - name: Test Plugin Tasks + if: ${{ !cancelled() }} + run: | + echo "Testing plugin scheduled tasks..." + cd ${GITHUB_WORKSPACE}/moodle + + echo "Running scan task..." + php admin/cli/scheduled_task.php --execute="local_cleanup\\task\\scan" || echo "Scan task completed with warnings (expected on empty test environment)" + + echo "Running cleanup task..." + php admin/cli/scheduled_task.php --execute="local_cleanup\\task\\cleanup" || echo "Cleanup task completed with warnings (expected on empty test environment)" + + echo "✅ Plugin tasks executed successfully" + + - name: Test CLI Scripts + if: ${{ !cancelled() }} + run: | + cd ${GITHUB_WORKSPACE}/moodle + + # Test usage statistics + if [ -f "local/cleanup/cli/usage_statistics.php" ]; then + echo "Testing usage statistics script..." + php local/cleanup/cli/usage_statistics.php + fi + + # Test reinit modules cleanup (with force flag) + if [ -f "local/cleanup/cli/reinit_modules_cleanup.php" ]; then + echo "Testing reinit modules cleanup script..." + php local/cleanup/cli/reinit_modules_cleanup.php --force + fi + + - name: Mark cancelled jobs as failed + if: ${{ cancelled() }} + run: exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..03aca46 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,211 @@ +name: Release + +on: + push: + tags: + - 'v*' + branches: + - master + - main + release: + types: [published] + +jobs: + validate-release: + name: Validate Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'release' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: intl, json, curl, zip, gd, mbstring, xml + coverage: none + + - name: Validate version.php + run: | + # Extract version from tag + TAG_VERSION=${GITHUB_REF#refs/tags/v} + echo "Tag version: $TAG_VERSION" + + # Extract version from version.php (format: YYYYMMDDXX) + PHP_VERSION=$(grep '$plugin->version' version.php | grep -oE '[0-9]+') + echo "PHP version: $PHP_VERSION" + + # Extract release version from version.php + RELEASE_VERSION=$(grep '$plugin->release' version.php | grep -oE "'[^']+'" | tr -d "'") + echo "Release version: $RELEASE_VERSION" + + # Validate that tag matches release version + if [ "$TAG_VERSION" != "$RELEASE_VERSION" ]; then + echo "ERROR: Tag version ($TAG_VERSION) doesn't match release version ($RELEASE_VERSION) in version.php" + exit 1 + fi + + echo "Version validation passed" + + - name: Check changelog + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "WARNING: No CHANGELOG.md found" + else + # Check if current version is documented + TAG_VERSION=${GITHUB_REF#refs/tags/v} + # Look for version in changelog (supports both [2.1] and 2.1 formats) + if ! grep -qE "\[$TAG_VERSION\]|## $TAG_VERSION" CHANGELOG.md; then + echo "WARNING: Version $TAG_VERSION not found in CHANGELOG.md" + echo "Looking for patterns: [$TAG_VERSION] or ## $TAG_VERSION" + else + echo "✅ Version $TAG_VERSION found in CHANGELOG.md" + fi + fi + + create-package: + name: Create Release Package + runs-on: ubuntu-latest + needs: validate-release + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'release' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Create release package + run: | + # Create a clean package without development files + mkdir -p release-package/local_cleanup + + # Copy essential files + rsync -av \ + --exclude='.git*' \ + --exclude='.github/' \ + --exclude='node_modules/' \ + --exclude='vendor/' \ + --exclude='*.md' \ + --exclude='composer.*' \ + --exclude='package*.json' \ + --exclude='*.yml' \ + --exclude='*.yaml' \ + --exclude='phpstan.neon' \ + --exclude='psalm.xml' \ + . release-package/local_cleanup/ + + # Create archive + cd release-package + tar -czf ../moodle-local_cleanup-${GITHUB_REF#refs/tags/}.tar.gz local_cleanup/ + zip -r ../moodle-local_cleanup-${GITHUB_REF#refs/tags/}.zip local_cleanup/ + cd .. + + # Generate checksums + sha256sum moodle-local_cleanup-${GITHUB_REF#refs/tags/}.tar.gz > checksums.txt + sha256sum moodle-local_cleanup-${GITHUB_REF#refs/tags/}.zip >> checksums.txt + + - name: Upload release artifacts + uses: actions/upload-artifact@v3 + with: + name: release-packages + path: | + moodle-local_cleanup-*.tar.gz + moodle-local_cleanup-*.zip + checksums.txt + + github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: create-package + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: release-packages + + - name: Extract release notes + id: release_notes + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + + if [ -f "CHANGELOG.md" ]; then + # Extract release notes for this version from CHANGELOG.md + awk "/^## \[$TAG_VERSION\]|^## $TAG_VERSION/{flag=1; next} /^## /{flag=0} flag" CHANGELOG.md > release_notes.txt + + if [ -s release_notes.txt ]; then + echo "Found release notes in CHANGELOG.md" + else + echo "No specific release notes found, using default" + echo "Release $TAG_VERSION of Moodle Clean-up Plugin" > release_notes.txt + fi + else + echo "Release $TAG_VERSION of Moodle Clean-up Plugin" > release_notes.txt + echo "" >> release_notes.txt + echo "See commit history for changes in this release." >> release_notes.txt + fi + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + body_path: release_notes.txt + files: | + moodle-local_cleanup-*.tar.gz + moodle-local_cleanup-*.zip + checksums.txt + draft: false + prerelease: ${{ contains(github.ref, '-') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + moodle-plugins-db: + name: Submit to Moodle Plugins Database + runs-on: ubuntu-latest + needs: github-release + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare submission info + run: | + echo "Plugin submission to Moodle Plugins Database would happen here" + echo "This requires:" + echo "1. Account on moodle.org" + echo "2. Plugin registered in the database" + echo "3. API access configured" + echo "" + echo "Manual steps:" + echo "1. Go to https://moodle.org/plugins/" + echo "2. Log in to your account" + echo "3. Navigate to your plugin page" + echo "4. Upload the new version package" + echo "5. Fill in the release notes" + echo "6. Submit for approval" + + notify: + name: Notify Release + runs-on: ubuntu-latest + needs: [github-release] + if: always() + + steps: + - name: Notify success + if: needs.github-release.result == 'success' + run: | + echo "✅ Release completed successfully!" + echo "Version: ${GITHUB_REF#refs/tags/}" + echo "Available at: https://github.com/${{ github.repository }}/releases/latest" + + - name: Notify failure + if: needs.github-release.result == 'failure' + run: | + echo "❌ Release failed!" + echo "Check the workflow logs for details." + exit 1 diff --git a/.gitignore b/.gitignore index 1da8522..23f60e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,107 @@ -.idea +# IDE and Editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# PHP specific +*.log +composer.phar +vendor/ +.phpunit.result.cache + +# Moodle specific +config.php +/config.php +config-dist.php + +# Moodle cache directories +/cache/ +/localcache/ +/temp/ +/tempdata/ + +# Moodle data directory (if accidentally placed in plugin) +/moodledata/ + +# Backup files +*.bak +*.backup +*.orig + +# Node.js (for themes/plugins with npm dependencies) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock + +# Build artifacts +/build/ +/dist/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Coverage directory used by tools like istanbul +coverage/ + +# Compiled binary addons +*.node + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# SCSS/CSS maps +*.css.map +*.scss.map + +# Compiled CSS from SCSS (if using automated compilation) +styles.css diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..482348b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to the Moodle Clean-up Plugin will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [2.2] - 2025-08-07 + +### Added +- Moodle Plugin CI workflow for automated code quality and standards checking + +### Changed +- Updated code style to match Moodle standards + +## [2.1] - 2025-08-02 + +### Fixed +- Check whether table `logstore_lanalytics_log` exists and skip its cleanup if not existent + +## [2.0] - 2024-07-05 + +### Added +- Logs clean-up +- Grades clean-up +- Course modules clean-up +- CLI script for fixing stuck course module deletions (`cli/reinit_modules_cleanup.php`) +- Statistics and usage reporting via CLI (`cli/usage_statistics.php`) +- Batch file removal operations + +### Changed +- Improved database cleanup, implemented dedicated clean-up steps +- Improved performance for large file operations + +### Removed +- Statistics and batch removal web UI + +## [1.4] - 2024-12-07 + +### Changed +- Compatibility improvements for Moodle 4.1 LTS + +## [1.3] - 2023-06-10 + +### Added +- Initial plugin release +- Files clean-up functionality +- Files clean-up management (web UI) + +## Compatibility + +| Version | Moodle | PHP | Status | +|---------|--------|-----|--------| +| 2.1 | 4.1+ | 7.4+ | ✅ Current | +| 2.0 | 4.0+ | 7.4+ | 📦 Archived | +| 1.x | 3.9+ | 7.2+ | ❌ EOL | diff --git a/README.md b/README.md index ca53c44..d61240a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Moodle Clean-up Plugin +[![Moodle Plugin CI](https://github.com/grinchenkoedu/local_cleanup/workflows/Moodle%20Plugin%20CI/badge.svg)](https://github.com/grinchenkoedu/local_cleanup/actions) +[![Release](https://github.com/grinchenkoedu/local_cleanup/workflows/Release/badge.svg)](https://github.com/grinchenkoedu/local_cleanup/actions) + A comprehensive Moodle plugin that manages and optimizes file storage and the database by automatically identifying and removing unnecessary files and records. diff --git a/classes/finder.php b/classes/finder.php index d43a5b6..c39ae1a 100644 --- a/classes/finder.php +++ b/classes/finder.php @@ -1,138 +1,199 @@ -db = $db; - } - - public function find(int $limit = self::LIMIT_DEFAULT, int $offset = 0, array $filter = []): moodle_recordset - { - return $this->db->get_recordset_sql( - $this->get_search_sql($filter, false, $limit, $offset), - $this->get_search_values($filter) - ); - } - - public function count(array $filter = []): int - { - return (int)$this->db->get_field_sql( - $this->get_search_sql($filter, true), - $this->get_search_values($filter) - ); - } - - /** - * @param string $component Component name - * @param string|null $until Date string for filtering (e.g., '-1 year') - * @param bool $newer_than If true, get files newer than $until; if false, get files older than $until - * @param string|null $from Date string for filtering (e.g., '-2 years') - * @return object {count: int, size: int (bytes)} - * - * @throws dml_exception - */ - public function stats(string $component, string $until = null, bool $newer_than = false, string $from = null) - { - $sql = ' - SELECT - COUNT(f.id) as `count`, - COALESCE(SUM(f.filesize), 0) as `size` - FROM {files} f - WHERE f.component = ? - '; - - // For backup component, use timemodified instead of timecreated - $timeField = ($component === 'backup') ? 'f.timemodified' : 'f.timecreated'; - - // If both from and until are provided, get files in the specific time period - if ($from !== null && $until !== null) { - $from_timestamp = strtotime($from); - $until_timestamp = strtotime($until); - $sql .= " AND $timeField >= $from_timestamp AND $timeField < $until_timestamp"; - - } else if ($until !== null) { - $operator = $newer_than ? '>' : '<'; - $sql .= " AND $timeField $operator " . strtotime($until); - } - - return $this->db->get_record_sql($sql, [$component]); - } - - private function get_search_values(array $filter): array - { - $values = []; - - if (!empty($filter['name_like'])) { - $values['name_like'] = '%' . $filter['name_like'] . '%'; - } - - if (!empty($filter['user_like'])) { - $values['user_like'] = '%' . $filter['user_like'] . '%'; - } - - if (!empty($filter['component'])) { - $values['component'] = $filter['component']; - } - - return $values; - } - - private function get_search_sql( - array $filter, bool - $count = false, - int $limit = self::LIMIT_DEFAULT, - $offset = 0 - ): string { - $where = [ - sprintf('f.filesize > %d', ($filter['filesize'] ?? 0) * 1024 * 1024) - ]; - - if (!empty($filter['component'])) { - $where[] = 'f.component = :component'; - } - - if (!empty($filter['name_like'])) { - $where[] = 'f.filename LIKE :name_like'; - } - - if (!empty($filter['user_like'])) { - $where[] = "(CONCAT(u.firstname, ' ', u.lastname) LIKE :user_like - OR CONCAT(u.lastname, ' ', u.firstname) LIKE :user_like - OR f.author LIKE :user_like)"; - } - - if (!empty($filter['user_deleted'])) { - $where[] = 'u.deleted = 1'; - } - - if ($count) { - return sprintf( - 'SELECT COUNT(f.id) FROM {files} f LEFT JOIN {user} u ON f.userid = u.id WHERE %s', - implode(' AND ', $where) - ); - } - - $userFields = fields::for_name() - ->get_sql('u', false, '', '', false) - ->selects; - - return sprintf( - 'SELECT %s FROM {files} f LEFT JOIN {user} u ON f.userid = u.id WHERE %s GROUP BY f.contenthash %s', - 'f.*, u.deleted as user_deleted, ' . $userFields, - implode(' AND ', $where), - $offset > 0 ? sprintf('LIMIT %d, %d', $offset, $limit) : sprintf('LIMIT %d', $limit) - ); - } -} +. + +namespace local_cleanup; + +use dml_exception; +use moodle_database; +use moodle_recordset; +use core_user\fields; + +/** + * File finder class for the cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class finder { + + /** Default limit for file queries */ + const LIMIT_DEFAULT = 50; + + /** + * Database connection instance. + * + * @var moodle_database + */ + private $db; + + /** + * Constructor. + * + * @param moodle_database $db Database connection + */ + public function __construct(moodle_database $db) { + $this->db = $db; + } + + /** + * Find files based on criteria. + * + * @param int $limit Maximum number of records to return + * @param int $offset Offset for pagination + * @param array $filter Filter criteria + * @return moodle_recordset Recordset of matching files + */ + public function find(int $limit = self::LIMIT_DEFAULT, int $offset = 0, array $filter = []): moodle_recordset { + return $this->db->get_recordset_sql( + $this->get_search_sql($filter, false, $limit, $offset), + $this->get_search_values($filter) + ); + } + + /** + * Count files matching the given filter criteria. + * + * @param array $filter Filter criteria + * @return int Number of matching files + */ + public function count(array $filter = []): int { + return (int)$this->db->get_field_sql( + $this->get_search_sql($filter, true), + $this->get_search_values($filter) + ); + } + + /** + * Get statistics for files by component. + * + * @param string $component Component name + * @param string|null $until Date string for filtering (e.g., '-1 year') + * @param bool $newerthan If true, get files newer than $until; if false, get files older than $until + * @param string|null $from Date string for filtering (e.g., '-2 years') + * @return object {count: int, size: int (bytes)} + * + * @throws dml_exception + */ + public function stats(string $component, ?string $until = null, bool $newerthan = false, ?string $from = null) { + $sql = ' + SELECT + COUNT(f.id) as "count", + COALESCE(SUM(f.filesize), 0) as "size" + FROM {files} f + WHERE f.component = ? + '; + + // For backup component, use timemodified instead of timecreated. + $timefield = ($component === 'backup') ? 'f.timemodified' : 'f.timecreated'; + + // If both from and until are provided, get files in the specific time period. + if ($from !== null && $until !== null) { + $fromtimestamp = strtotime($from); + $untiltimestamp = strtotime($until); + $sql .= " AND $timefield >= $fromtimestamp AND $timefield < $untiltimestamp"; + + } else if ($until !== null) { + $operator = $newerthan ? '>' : '<'; + $sql .= " AND $timefield $operator " . strtotime($until); + } + + return $this->db->get_record_sql($sql, [$component]); + } + + /** + * Get search parameter values for SQL queries. + * + * @param array $filter Filter criteria + * @return array Parameter values for SQL query + */ + private function get_search_values(array $filter): array { + $values = []; + + if (!empty($filter['name_like'])) { + $values['name_like'] = '%' . $filter['name_like'] . '%'; + } + + if (!empty($filter['user_like'])) { + $values['user_like'] = '%' . $filter['user_like'] . '%'; + } + + if (!empty($filter['component'])) { + $values['component'] = $filter['component']; + } + + return $values; + } + + /** + * Build SQL query for file search. + * + * @param array $filter Filter criteria + * @param bool $count Whether this is for counting (true) or selecting records (false) + * @param int $limit Maximum number of records to return + * @param int $offset Offset for pagination + * @return string SQL query + */ + private function get_search_sql( + array $filter, bool + $count = false, + int $limit = self::LIMIT_DEFAULT, + $offset = 0 + ): string { + $where = [ + sprintf('f.filesize > %d', ($filter['filesize'] ?? 0) * 1024 * 1024), + ]; + + if (!empty($filter['component'])) { + $where[] = 'f.component = :component'; + } + + if (!empty($filter['name_like'])) { + $where[] = 'f.filename LIKE :name_like'; + } + + if (!empty($filter['user_like'])) { + // Use database-agnostic concatenation via Moodle's sql_concat. + $fullname1 = $this->db->sql_concat('u.firstname', "' '", 'u.lastname'); + $fullname2 = $this->db->sql_concat('u.lastname', "' '", 'u.firstname'); + $where[] = "($fullname1 LIKE :user_like" + . " OR $fullname2 LIKE :user_like" + . " OR f.author LIKE :user_like)"; + } + + if (!empty($filter['user_deleted'])) { + $where[] = 'u.deleted = 1'; + } + + if ($count) { + return sprintf( + 'SELECT COUNT(f.id) FROM {files} f LEFT JOIN {user} u ON f.userid = u.id WHERE %s', + implode(' AND ', $where) + ); + } + + $userfields = fields::for_name() + ->get_sql('u', false, '', '', false) + ->selects; + + return sprintf( + 'SELECT %s FROM {files} f LEFT JOIN {user} u ON f.userid = u.id WHERE %s GROUP BY f.contenthash %s', + 'f.*, u.deleted as user_deleted, ' . $userfields, + implode(' AND ', $where), + $offset > 0 ? sprintf('LIMIT %d OFFSET %d', $limit, $offset) : sprintf('LIMIT %d', $limit) + ); + } +} diff --git a/classes/form/filter_form.php b/classes/form/filter_form.php index c55f280..374c45e 100644 --- a/classes/form/filter_form.php +++ b/classes/form/filter_form.php @@ -1,66 +1,93 @@ -_form; - $filesize = $this->_customdata['filesize'] ?? 0; - $name_like = $this->_customdata['name_like'] ?? ''; - $user_like = $this->_customdata['user_like'] ?? ''; - $user_deleted = $this->_customdata['user_deleted'] ?? false; - $component = $this->_customdata['component'] ?? null; - - $form->addElement('header', 'header', get_string('filter')); - $form->setExpanded( - 'header', - !empty($name_like) || !empty($user_like) || !empty($component) - ); - - $form->addElement('text', 'name_like', get_string('filename', 'backup')); - $form->setType('name_like', PARAM_TEXT); - $form->setDefault('name_like', $name_like); - - $form->addElement('text', 'user_like', get_string('user', 'admin')); - $form->setType('user_like', PARAM_TEXT); - $form->setDefault('user_like', $user_like); - - $form->addElement('checkbox', 'user_deleted', 'Deleted users'); - $form->setDefault('user_deleted', $user_deleted); - - $form->addElement('select', 'component', get_string('module', 'backup'), [ - null => '-', - 'tool_recyclebin' => get_string('pluginname', 'tool_recyclebin'), - 'backup' => get_string('backup'), - 'user' => get_string('user', 'admin'), - ]); - $form->setDefault('component', $component); - - $form->addElement('select', 'filesize', '>=', [ - 0 => '-', - 10 => '10 MB', - 50 => '50 MB', - 100 => '100 MB', - 200 => '200 MB', - 500 => '500 MB', - 1000 => '1 GB', - ]); - $form->setDefault('filesize', $filesize); - - $form->addGroup($this->getButtons(), 'buttonarr', '', [' '], false); - - $form->disable_form_change_checker(); - } - - private function getButtons() - { - return [ - $this->_form->createElement('submit', 'submitbutton', get_string('search')), - $this->_form->createElement('cancel') - ]; - } -} +. + +namespace local_cleanup\form; + +use moodleform; + +/** + * Filter form for files search. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class filter_form extends moodleform { + + /** + * Define the form elements. + */ + protected function definition() { + $form = $this->_form; + $filesize = $this->_customdata['filesize'] ?? 0; + $namelike = $this->_customdata['name_like'] ?? ''; + $userlike = $this->_customdata['user_like'] ?? ''; + $userdeleted = $this->_customdata['user_deleted'] ?? false; + $component = $this->_customdata['component'] ?? null; + + $form->addElement('header', 'header', get_string('filter')); + $form->setExpanded( + 'header', + !empty($namelike) || !empty($userlike) || !empty($component) + ); + + $form->addElement('text', 'name_like', get_string('filename', 'backup')); + $form->setType('name_like', PARAM_TEXT); + $form->setDefault('name_like', $namelike); + + $form->addElement('text', 'user_like', get_string('user', 'admin')); + $form->setType('user_like', PARAM_TEXT); + $form->setDefault('user_like', $userlike); + + $form->addElement('checkbox', 'user_deleted', 'Deleted users'); + $form->setDefault('user_deleted', $userdeleted); + + $form->addElement('select', 'component', get_string('module', 'backup'), [ + null => '-', + 'tool_recyclebin' => get_string('pluginname', 'tool_recyclebin'), + 'backup' => get_string('backup'), + 'user' => get_string('user', 'admin'), + ]); + $form->setDefault('component', $component); + + $form->addElement('select', 'filesize', '>=', [ + 0 => '-', + 10 => '10 MB', + 50 => '50 MB', + 100 => '100 MB', + 200 => '200 MB', + 500 => '500 MB', + 1000 => '1 GB', + ]); + $form->setDefault('filesize', $filesize); + + $form->addGroup($this->getButtons(), 'buttonarr', '', [' '], false); + + $form->disable_form_change_checker(); + } + + /** + * Get the form buttons. + * + * @return array Array of form elements for the buttons + */ + private function getbuttons() { + return [ + $this->_form->createElement('submit', 'submitbutton', get_string('search')), + $this->_form->createElement('cancel'), + ]; + } +} diff --git a/classes/output/MtraceOutput.php b/classes/output/MtraceOutput.php index a7b54a0..e8369bd 100644 --- a/classes/output/MtraceOutput.php +++ b/classes/output/MtraceOutput.php @@ -1,16 +1,47 @@ . namespace local_cleanup\output; -class MtraceOutput implements OutputInterface -{ - public function write(string $message) - { +/** + * Output handler that uses Moodle's mtrace function. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class MtraceOutput implements OutputInterface { + + /** + * Write a message without a line break. + * + * @param string $message The message to write + * @return void + */ + public function write(string $message) { mtrace($message, null); } - public function writeLine(string $message) - { + /** + * Write a message with a line break. + * + * @param string $message The message to write + * @return void + */ + public function writeline(string $message) { mtrace($message); } } diff --git a/classes/output/OutputInterface.php b/classes/output/OutputInterface.php index c69d6e4..5318d58 100644 --- a/classes/output/OutputInterface.php +++ b/classes/output/OutputInterface.php @@ -1,9 +1,43 @@ . namespace local_cleanup\output; -interface OutputInterface -{ +/** + * Interface for output handlers. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface OutputInterface { + + /** + * Write a message without a line break. + * + * @param string $message The message to write + * @return void + */ public function write(string $message); - public function writeLine(string $message); + + /** + * Write a message with a line break. + * + * @param string $message The message to write + * @return void + */ + public function writeline(string $message); } diff --git a/classes/steps/AbstractCleanupStep.php b/classes/steps/AbstractCleanupStep.php index cd0f029..9c545ae 100644 --- a/classes/steps/AbstractCleanupStep.php +++ b/classes/steps/AbstractCleanupStep.php @@ -1,32 +1,73 @@ . namespace local_cleanup\steps; use local_cleanup\output\OutputInterface; use moodle_database; -abstract class AbstractCleanupStep implements CleanupStepInterface -{ - protected moodle_database $db; +/** + * Abstract base class for cleanup steps. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class AbstractCleanupStep implements CleanupStepInterface { + + /** + * Database connection. + * + * @var moodle_database + */ + protected $db; + + /** + * Maximum number of records to process in a single batch. + */ const BATCH_SIZE = 999; - public function __construct(moodle_database $db) - { + /** + * Constructor. + * + * @param moodle_database $db Database connection + */ + public function __construct(moodle_database $db) { $this->db = $db; } - abstract public function cleanUp(OutputInterface $output); + /** + * Execute the cleanup step. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + abstract public function cleanup(OutputInterface $output); /** * Process records in batches and delete them * * @param string $table The database table name + * @param string $alias The table alias used in the SQL query * @param string $sql The SQL query to find records to delete * @param array $params The parameters for the SQL query * @param string $message The message to display when checking for records * @param OutputInterface $output The output interface for logging */ - protected function processRecordsInBatches( + protected function processrecordsinbatches( $table, $alias, $sql, @@ -37,67 +78,67 @@ protected function processRecordsInBatches( $output->writeLine(sprintf('Cleaning %s: %s', $table, $message)); $limit = self::BATCH_SIZE * 100; - $totalDeleted = 0; - $batchNumber = 0; - $lastId = 0; + $totaldeleted = 0; + $batchnumber = 0; + $lastid = 0; do { - if ($batchNumber > 0) { + if ($batchnumber > 0) { $output->writeLine( sprintf( 'Cleaning %s: Loading batch %d...', $table, - $batchNumber + 1 + $batchnumber + 1 ) ); } - $boundedSql = sprintf( + $boundedsql = sprintf( '%s AND %s.id > :lastid ORDER BY %s.id ASC LIMIT %d', $sql, $alias, $alias, $limit ); - $boundedParams = array_merge($params, ['lastid' => $lastId]); + $boundedparams = array_merge($params, ['lastid' => $lastid]); - $startTime = microtime(true); + $starttime = microtime(true); - $ids = $this->db->get_fieldset_sql($boundedSql, $boundedParams); - $lastId = end($ids); + $ids = $this->db->get_fieldset_sql($boundedsql, $boundedparams); + $lastid = end($ids); $count = count($ids); - $batchNumber++; + $batchnumber++; if ($count > 0) { $output->write('Deleting..'); while (!empty($ids)) { - $batchIds = array_splice($ids, 0, self::BATCH_SIZE); - $batchCount = count($batchIds); - $totalDeleted += $batchCount; + $batchids = array_splice($ids, 0, self::BATCH_SIZE); + $batchcount = count($batchids); + $totaldeleted += $batchcount; - $this->db->delete_records_list($table, 'id', $batchIds); + $this->db->delete_records_list($table, 'id', $batchids); $output->write('.'); } - $endTime = microtime(true); - $elapsedSeconds = $endTime - $startTime; + $endtime = microtime(true); + $elapsedseconds = $endtime - $starttime; $output->writeLine( sprintf( 'OK (took %02d:%02d)', - floor($elapsedSeconds / 60), - floor($elapsedSeconds % 60) + floor($elapsedseconds / 60), + floor($elapsedseconds % 60) ) ); } } while ($count === $limit); - if ($totalDeleted === 0) { + if ($totaldeleted === 0) { $output->writeLine('None found.'); return; } - $output->writeLine("Total records deleted: $totalDeleted. Done."); + $output->writeLine("Total records deleted: $totaldeleted. Done."); } } diff --git a/classes/steps/CleanupStepInterface.php b/classes/steps/CleanupStepInterface.php index beb8321..4bd35d7 100644 --- a/classes/steps/CleanupStepInterface.php +++ b/classes/steps/CleanupStepInterface.php @@ -1,10 +1,37 @@ . namespace local_cleanup\steps; use local_cleanup\output\OutputInterface; -interface CleanupStepInterface -{ - public function cleanUp(OutputInterface $output); +/** + * Interface for cleanup steps. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface CleanupStepInterface { + + /** + * Execute the cleanup step. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output); } diff --git a/classes/steps/ComponentFilesCleanup.php b/classes/steps/ComponentFilesCleanup.php index 80ae76b..8507299 100644 --- a/classes/steps/ComponentFilesCleanup.php +++ b/classes/steps/ComponentFilesCleanup.php @@ -1,37 +1,87 @@ . namespace local_cleanup\steps; use local_cleanup\output\OutputInterface; use moodle_database; -class ComponentFilesCleanup extends AbstractCleanupStep -{ +/** + * Component files cleanup step. + * + * Handles cleanup of files from specific components based on age. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ComponentFilesCleanup extends AbstractCleanupStep { + + /** + * Default number of days to keep component files. + */ const DEFAULT_LIFETIME_DAYS = 180; - private array $components; - private int $daysToKeep; + /** + * List of components to clean up. + * + * @var array + */ + private $components; + + /** + * Number of days to keep files. + * + * @var int + */ + private $daystokeep; - public function __construct(moodle_database $db, array $components, int $daysToKeep = self::DEFAULT_LIFETIME_DAYS) - { + /** + * Constructor. + * + * @param moodle_database $db Database connection + * @param array $components List of component names to clean up + * @param int $daystokeep Number of days to keep files + */ + public function __construct(moodle_database $db, array $components, int $daystokeep = self::DEFAULT_LIFETIME_DAYS) { parent::__construct($db); $this->components = $components; - $this->daysToKeep = $daysToKeep; + $this->daystokeep = $daystokeep; } - public function cleanUp(OutputInterface $output) - { + /** + * Execute the cleanup step. + * + * Removes files from specified components that are older than the configured days to keep. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output) { $output->writeLine('Starting component files cleanup...'); - $cutoffDate = time() - ($this->daysToKeep * 24 * 60 * 60); + $cutoffdate = time() - ($this->daystokeep * 24 * 60 * 60); foreach ($this->components as $component) { $output->writeLine("Processing component '$component'..."); - + $params = [ 'component' => $component, - 'cutoffdate' => $cutoffDate + 'cutoffdate' => $cutoffdate, ]; $sql = "SELECT f.id diff --git a/classes/steps/CourseModulesCleanup.php b/classes/steps/CourseModulesCleanup.php index 15781cf..c4046c2 100644 --- a/classes/steps/CourseModulesCleanup.php +++ b/classes/steps/CourseModulesCleanup.php @@ -1,4 +1,18 @@ . namespace local_cleanup\steps; @@ -6,35 +20,64 @@ use local_cleanup\output\OutputInterface; use moodle_database; -class CourseModulesCleanup implements CleanupStepInterface -{ +/** + * Course modules cleanup step. + * + * Handles cleanup of orphaned course modules and failed course module deletion tasks. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class CourseModulesCleanup implements CleanupStepInterface { + + /** + * Default number of days to keep course modules. + */ const DEFAULT_LIFETIME_DAYS = 7; - private moodle_database $db; + /** + * Database connection. + * + * @var moodle_database + */ + private $db; /** * @var int Number of days to keep course modules */ - private $daysToKeep; + private $daystokeep; - public function __construct(moodle_database $db, int $daysToKeep = self::DEFAULT_LIFETIME_DAYS) - { + /** + * Constructor. + * + * @param moodle_database $db Database connection + * @param int $daystokeep Number of days to keep course modules + */ + public function __construct(moodle_database $db, int $daystokeep = self::DEFAULT_LIFETIME_DAYS) { $this->db = $db; - $this->daysToKeep = $daysToKeep; + $this->daystokeep = $daystokeep; } - public function cleanUp(OutputInterface $output) - { + /** + * Execute the cleanup step. + * + * Cleans up orphaned course modules and failed course module deletion tasks. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output) { global $CFG; $this->cleanUpOrphanedCourseModules($output); - $cutoffTime = time() - ($this->daysToKeep * 24 * 60 * 60); + $cutofftime = time() - ($this->daystokeep * 24 * 60 * 60); $tasks = $this->db->get_records_select( 'task_adhoc', 'classname = ? AND faildelay > 0 AND timestarted < ?', - ['\core_course\task\course_delete_modules', $cutoffTime] + ['\core_course\task\course_delete_modules', $cutofftime] ); if (count($tasks) === 0) { @@ -62,8 +105,17 @@ public function cleanUp(OutputInterface $output) } } - private function deleteCourseModule(int $id, OutputInterface $output) - { + /** + * Delete a course module by ID. + * + * Attempts to delete a course module using the standard Moodle function, + * and falls back to manual cleanup if that fails. + * + * @param int $id Course module ID + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function deletecoursemodule(int $id, OutputInterface $output) { $output->write(sprintf('Deleting course module %d...', $id)); $cm = $this->db->get_record('course_modules', ['id' => $id]); @@ -90,29 +142,36 @@ private function deleteCourseModule(int $id, OutputInterface $output) $output->writeLine('OK'); } - private function cleanUpOrphanedCourseModules(OutputInterface $output): void - { + /** + * Clean up course modules that are tied to deleted courses. + * + * Identifies and removes course modules that reference courses that no longer exist. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanuporphanedcoursemodules(OutputInterface $output): void { global $CFG; $output->writeLine('Checking for course modules tied to deleted courses...'); - $sql = "SELECT cm.* - FROM {course_modules} cm - LEFT JOIN {course} c ON cm.course = c.id - WHERE c.id IS NULL"; + $sql = "SELECT cm.* " + . "FROM {course_modules} cm " + . "LEFT JOIN {course} c ON cm.course = c.id " + . "WHERE c.id IS NULL"; - $orphanedModules = $this->db->get_records_sql($sql); + $orphanedmodules = $this->db->get_records_sql($sql); - if (empty($orphanedModules)) { + if (empty($orphanedmodules)) { $output->writeLine('No orphaned course modules found.'); return; } - $output->writeLine(sprintf('Found %d orphaned course modules. Cleaning up...', count($orphanedModules))); + $output->writeLine(sprintf('Found %d orphaned course modules. Cleaning up...', count($orphanedmodules))); require_once($CFG->dirroot . '/course/lib.php'); - foreach ($orphanedModules as $cm) { + foreach ($orphanedmodules as $cm) { try { $this->deleteCourseModule($cm->id, $output); } catch (Exception $e) { @@ -124,14 +183,18 @@ private function cleanUpOrphanedCourseModules(OutputInterface $output): void } /** - * This is the clean-up part of the course_delete_module function, Moodle 4.1 - * @param object $cm + * Manually clean up course module data when standard deletion fails. + * + * This is based on the clean-up part of the course_delete_module function in Moodle 4.1. + * It removes all associated data for a course module when the standard deletion process fails. + * + * @param object $cm Course module object + * @return void * @see course_delete_module */ - private function cleanUpCourseModuleData($cm): void - { + private function cleanupcoursemoduledata($cm): void { $modcontext = \context_module::instance($cm->id); - $modulename = $this->db->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); + $modulename = $this->db->get_field('modules', 'name', ['id' => $cm->module], MUST_EXIST); question_delete_activity($cm); @@ -140,9 +203,9 @@ private function cleanUpCourseModuleData($cm): void $fs->delete_area_files($modcontext->id); // Delete events from calendar. - if ($events = $this->db->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) { + if ($events = $this->db->get_records('event', ['instance' => $cm->instance, 'modulename' => $modulename])) { $coursecontext = \context_course::instance($cm->course); - foreach($events as $event) { + foreach ($events as $event) { $event->context = $coursecontext; $calendarevent = \calendar_event::load($event); $calendarevent->delete(); @@ -150,10 +213,10 @@ private function cleanUpCourseModuleData($cm): void } // Delete grade items, outcome items and grades attached to modules. - if ($grade_items = \grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename, - 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) { - foreach ($grade_items as $grade_item) { - $grade_item->delete('moddelete'); + if ($gradeitems = \grade_item::fetch_all(['itemtype' => 'mod', 'itemmodule' => $modulename, + 'iteminstance' => $cm->instance, 'courseid' => $cm->course])) { + foreach ($gradeitems as $gradeitem) { + $gradeitem->delete('moddelete'); } } @@ -163,11 +226,11 @@ private function cleanUpCourseModuleData($cm): void // Delete completion and availability data; it is better to do this even if the // features are not turned on, in case they were turned on previously (these will be // very quick on an empty table). - $this->db->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id)); + $this->db->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]); $this->db->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]); - $this->db->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id, + $this->db->delete_records('course_completion_criteria', ['moduleinstance' => $cm->id, 'course' => $cm->course, - 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY)); + 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY]); // Delete all tag instances associated with the instance of this module. \core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id); @@ -180,7 +243,7 @@ private function cleanUpCourseModuleData($cm): void \context_helper::delete_instance(CONTEXT_MODULE, $cm->id); // Delete the module from the course_modules table. - $this->db->delete_records('course_modules', array('id' => $cm->id)); + $this->db->delete_records('course_modules', ['id' => $cm->id]); // Delete module from that section. if (!delete_mod_from_section($cm->id, $cm->section)) { @@ -189,15 +252,15 @@ private function cleanUpCourseModuleData($cm): void } // Trigger event for course module delete action. - $event = \core\event\course_module_deleted::create(array( + $event = \core\event\course_module_deleted::create([ 'courseid' => $cm->course, 'context' => $modcontext, 'objectid' => $cm->id, - 'other' => array( + 'other' => [ 'modulename' => $modulename, 'instanceid' => $cm->instance, - ) - )); + ], + ]); $event->add_record_snapshot('course_modules', $cm); $event->trigger(); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); diff --git a/classes/steps/FilesCheckout.php b/classes/steps/FilesCheckout.php index 11fd161..898a691 100644 --- a/classes/steps/FilesCheckout.php +++ b/classes/steps/FilesCheckout.php @@ -1,4 +1,18 @@ . namespace local_cleanup\steps; @@ -6,30 +20,84 @@ use local_cleanup\output\OutputInterface; use moodle_database; -class FilesCheckout implements CleanupStepInterface -{ +/** + * Files checkout cleanup step. + * + * Handles cleanup of backup and draft files based on configured timeouts. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class FilesCheckout implements CleanupStepInterface { + + /** + * Empty string for selecting all records. + */ const SELECT_ALL = ''; - const DEFAULT_TIMEOUT_DAYS = 30; - private moodle_database $db; - private file_storage $fs; - private int $backupTimeout; - private int $draftTimeout; + /** + * Default timeout in days for file removal. + */ + const DEFAULT_TIMEOUT_DAYS = 30; + /** + * Database connection. + * + * @var moodle_database + */ + private $db; + + /** + * File storage instance. + * + * @var file_storage + */ + private $fs; + + /** + * Backup files timeout in seconds. + * + * @var int + */ + private $backuptimeout; + + /** + * Draft files timeout in seconds. + * + * @var int + */ + private $drafttimeout; + + /** + * Constructor. + * + * @param moodle_database $db Database connection + * @param file_storage $fs File storage instance + * @param int $backuptimeoutdays Number of days to keep backup files + * @param int $drafttimeoutdays Number of days to keep draft files + */ public function __construct( moodle_database $db, file_storage $fs, - int $backupTimeoutDays = self::DEFAULT_TIMEOUT_DAYS, - int $draftTimeoutDays = self::DEFAULT_TIMEOUT_DAYS + int $backuptimeoutdays = self::DEFAULT_TIMEOUT_DAYS, + int $drafttimeoutdays = self::DEFAULT_TIMEOUT_DAYS ) { $this->db = $db; $this->fs = $fs; - $this->backupTimeout = $backupTimeoutDays * 24 * 60 * 60; - $this->draftTimeout = $draftTimeoutDays * 24 * 60 * 60; + $this->backuptimeout = $backuptimeoutdays * 24 * 60 * 60; + $this->drafttimeout = $drafttimeoutdays * 24 * 60 * 60; } - public function cleanUp(OutputInterface $output) - { + /** + * Execute the cleanup step. + * + * Checks all files and removes outdated backups and draft files. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output) { $output->write('Fetching records... '); $ids = $this->db->get_fieldset_select('files', 'id', self::SELECT_ALL); @@ -55,12 +123,18 @@ public function cleanUp(OutputInterface $output) } } - private function checkout($id, OutputInterface $output): bool - { + /** + * Check a file and remove it if it's outdated. + * + * @param int $id File ID to check + * @param OutputInterface $output Output handler for logging + * @return bool True if the file should be kept, false if it was removed + */ + private function checkout($id, OutputInterface $output): bool { $file = $this->fs->get_file_by_id($id); if ($file === false) { - // wrong id provided (maybe already removed), continue... + // Wrong id provided (maybe already removed), continue... return true; } @@ -77,7 +151,7 @@ private function checkout($id, OutputInterface $output): bool if ( preg_match('/\.mbz$/', $file->get_filename()) - && $file->get_timecreated() <= time() - $this->backupTimeout + && $file->get_timecreated() <= time() - $this->backuptimeout ) { unlink($uri); $output->writeLine(sprintf( @@ -91,7 +165,7 @@ private function checkout($id, OutputInterface $output): bool if ( $file->get_filearea() === 'draft' - && $file->get_timecreated() <= time() - $this->draftTimeout + && $file->get_timecreated() <= time() - $this->drafttimeout && 1 === $this->db->count_records('files', ['contenthash' => $file->get_contenthash()]) ) { unlink($uri); diff --git a/classes/steps/GhostFilesCleanup.php b/classes/steps/GhostFilesCleanup.php index a6d4a04..d4c16f3 100644 --- a/classes/steps/GhostFilesCleanup.php +++ b/classes/steps/GhostFilesCleanup.php @@ -1,29 +1,75 @@ . namespace local_cleanup\steps; use local_cleanup\output\OutputInterface; use moodle_database; -class GhostFilesCleanup implements CleanupStepInterface -{ - private moodle_database $db; - private string $dataRoot; +/** + * Ghost files cleanup step. + * + * Removes files that are tracked in the cleanup table but no longer referenced in the files table. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class GhostFilesCleanup implements CleanupStepInterface { - public function __construct(moodle_database $db, string $dataRoot) - { + /** + * Database connection. + * + * @var moodle_database + */ + private $db; + + /** + * Moodle data root directory path. + * + * @var string + */ + private $dataroot; + + /** + * Constructor. + * + * @param moodle_database $db Database connection + * @param string $dataroot Path to Moodle data directory + */ + public function __construct(moodle_database $db, string $dataroot) { $this->db = $db; - $this->dataRoot = $dataRoot; + $this->dataroot = $dataroot; } - public function cleanUp(OutputInterface $output) - { + /** + * Execute the cleanup step. + * + * Removes ghost files that are tracked in the cleanup table. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output) { $output->write('Deleting unlinked files... '); - $ghost_files = $this->db->get_recordset('cleanup', [], '', 'id, path'); + $ghostfiles = $this->db->get_recordset('local_cleanup_files', [], '', 'id, path'); - foreach ($ghost_files as $item) { - $path = $this->dataRoot . DIRECTORY_SEPARATOR . $item->path; + foreach ($ghostfiles as $item) { + $path = $this->dataroot . DIRECTORY_SEPARATOR . $item->path; if (file_exists($path) && unlink($path)) { $output->write('.'); @@ -31,7 +77,7 @@ public function cleanUp(OutputInterface $output) $output->write('E'); } - $this->db->delete_records('cleanup', ['id' => $item->id]); + $this->db->delete_records('local_cleanup_files', ['id' => $item->id]); } $output->writeLine('Done!'); diff --git a/classes/steps/GradesCleanup.php b/classes/steps/GradesCleanup.php index 62ab41d..609f9d8 100644 --- a/classes/steps/GradesCleanup.php +++ b/classes/steps/GradesCleanup.php @@ -1,25 +1,69 @@ . namespace local_cleanup\steps; use local_cleanup\output\OutputInterface; use moodle_database; -class GradesCleanup extends AbstractCleanupStep -{ +/** + * Grades cleanup step. + * + * Handles cleanup of orphaned grade records, including grade items, grades, + * categories, and history records. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class GradesCleanup extends AbstractCleanupStep { + + /** + * Default number of days to keep grade history records. + */ const DEFAULT_LIFETIME_DAYS = 500; - private int $daysToKeep; + /** + * Number of days to keep grade history records. + * + * @var int + */ + private $daystokeep; - public function __construct(moodle_database $db, int $daysToKeep = self::DEFAULT_LIFETIME_DAYS) - { + /** + * Constructor. + * + * @param moodle_database $db Database connection + * @param int $daystokeep Number of days to keep grade history records + */ + public function __construct(moodle_database $db, int $daystokeep = self::DEFAULT_LIFETIME_DAYS) { parent::__construct($db); - $this->daysToKeep = $daysToKeep; + $this->daystokeep = $daystokeep; } - public function cleanUp(OutputInterface $output) - { + /** + * Execute the cleanup step. + * + * Runs all grade cleanup operations in sequence. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output) { $output->writeLine('Starting grades cleanup...'); // 1. Clean up grade items tied to deleted courses. @@ -46,8 +90,13 @@ public function cleanUp(OutputInterface $output) $output->writeLine('Grades cleanup completed.'); } - private function cleanupGradeItemsForDeletedCourses(OutputInterface $output) - { + /** + * Clean up grade items tied to deleted courses. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupgradeitemsfordeletedcourses(OutputInterface $output) { $sql = "SELECT gi.id FROM {grade_items} gi LEFT JOIN {course} c ON gi.courseid = c.id @@ -64,8 +113,13 @@ private function cleanupGradeItemsForDeletedCourses(OutputInterface $output) ); } - private function cleanupGradeItemsForDeletedModules(OutputInterface $output) - { + /** + * Clean up grade items for modules that no longer exist. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupgradeitemsfordeletedmodules(OutputInterface $output) { $sql = "SELECT gi.id FROM {grade_items} gi WHERE gi.itemtype = 'mod' @@ -85,8 +139,13 @@ private function cleanupGradeItemsForDeletedModules(OutputInterface $output) ); } - private function cleanupOrphanedGradeGrades(OutputInterface $output) - { + /** + * Clean up grade grades with no corresponding grade items. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanuporphanedgradegrades(OutputInterface $output) { $sql = "SELECT gg.id FROM {grade_grades} gg LEFT JOIN {grade_items} gi ON gi.id = gg.itemid @@ -102,8 +161,13 @@ private function cleanupOrphanedGradeGrades(OutputInterface $output) ); } - private function cleanupGradeGradesForDeletedUsers(OutputInterface $output) - { + /** + * Clean up grade grades for deleted users. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupgradegradesfordeletedusers(OutputInterface $output) { $sql = "SELECT gg.id FROM {grade_grades} gg LEFT JOIN {user} u ON gg.userid = u.id @@ -119,8 +183,13 @@ private function cleanupGradeGradesForDeletedUsers(OutputInterface $output) ); } - private function cleanupGradeCategoriesForDeletedCourses(OutputInterface $output) - { + /** + * Clean up grade categories tied to deleted courses. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupgradecategoriesfordeletedcourses(OutputInterface $output) { $sql = "SELECT gc.id FROM {grade_categories} gc LEFT JOIN {course} c ON gc.courseid = c.id @@ -136,8 +205,13 @@ private function cleanupGradeCategoriesForDeletedCourses(OutputInterface $output ); } - private function cleanupGradeOutcomesCoursesForDeletedCourses(OutputInterface $output) - { + /** + * Clean up grade outcomes courses tied to deleted courses. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupgradeoutcomescoursesfordeletedcourses(OutputInterface $output) { $sql = "SELECT goc.id FROM {grade_outcomes_courses} goc LEFT JOIN {course} c ON goc.courseid = c.id @@ -153,9 +227,14 @@ private function cleanupGradeOutcomesCoursesForDeletedCourses(OutputInterface $o ); } - private function cleanupGradeGradesHistory(OutputInterface $output) - { - $cutoffDate = time() - ($this->daysToKeep * 24 * 60 * 60); + /** + * Clean up grade grades history with no corresponding grade items or older than the configured days to keep. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupgradegradeshistory(OutputInterface $output) { + $cutoffdate = time() - ($this->daystokeep * 24 * 60 * 60); $sql = "SELECT ggh.id FROM {grade_grades_history} ggh @@ -166,10 +245,10 @@ private function cleanupGradeGradesHistory(OutputInterface $output) 'grade_grades_history', 'ggh', $sql, - ['cutoffdate' => $cutoffDate], + ['cutoffdate' => $cutoffdate], sprintf( 'Checking for grade grades history with no corresponding grade items or older than %d days...', - $this->daysToKeep + $this->daystokeep ), $output ); diff --git a/classes/steps/LogsCleanup.php b/classes/steps/LogsCleanup.php index 62f0672..82ab947 100644 --- a/classes/steps/LogsCleanup.php +++ b/classes/steps/LogsCleanup.php @@ -1,27 +1,76 @@ . namespace local_cleanup\steps; use local_cleanup\output\OutputInterface; use moodle_database; -class LogsCleanup extends AbstractCleanupStep -{ +/** + * Logs cleanup step. + * + * Handles cleanup of standard and analytics logs based on age. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class LogsCleanup extends AbstractCleanupStep { + + /** + * Default number of days to keep logs. + */ const DEFAULT_LIFETIME_DAYS = 500; - private int $cutoffDate; - private int $cutoffDays; + /** + * Cutoff timestamp for log deletion. + * + * @var int + */ + private $cutoffdate; + + /** + * Number of days to keep logs. + * + * @var int + */ + private $cutoffdays; - public function __construct(moodle_database $db, int $daysToKeep = self::DEFAULT_LIFETIME_DAYS) - { + /** + * Constructor. + * + * @param moodle_database $db Database connection + * @param int $daystokeep Number of days to keep logs + */ + public function __construct(moodle_database $db, int $daystokeep = self::DEFAULT_LIFETIME_DAYS) { parent::__construct($db); - $this->cutoffDate = time() - $daysToKeep * 24 * 60 * 60; - $this->cutoffDays = $daysToKeep; + $this->cutoffdate = time() - $daystokeep * 24 * 60 * 60; + $this->cutoffdays = $daystokeep; } - public function cleanUp(OutputInterface $output) - { + /** + * Execute the cleanup step. + * + * Cleans up both standard and analytics logs. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + public function cleanup(OutputInterface $output) { $output->writeLine('Starting logs cleanup...'); $this->cleanupStandardLogs($output); @@ -30,8 +79,13 @@ public function cleanUp(OutputInterface $output) $output->writeLine('Logs cleanup completed.'); } - private function cleanupStandardLogs(OutputInterface $output) - { + /** + * Clean up standard logs that are obsolete or older than the configured days to keep. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanupstandardlogs(OutputInterface $output) { $sql = "SELECT l.id FROM {logstore_standard_log} l LEFT JOIN {context} ctx ON ctx.id = l.contextid @@ -42,17 +96,22 @@ private function cleanupStandardLogs(OutputInterface $output) 'logstore_standard_log', 'l', $sql, - ['cutoffdate' => $this->cutoffDate], + ['cutoffdate' => $this->cutoffdate], sprintf( 'Checking for logs to clean up (obsolete or older than %d days)...', - $this->cutoffDays + $this->cutoffdays ), $output ); } - private function cleanupLAnalyticsLogs(OutputInterface $output) - { + /** + * Clean up learning analytics logs that are obsolete or older than the configured days to keep. + * + * @param OutputInterface $output Output handler for logging + * @return void + */ + private function cleanuplanalyticslogs(OutputInterface $output) { if (!$this->db->get_manager()->table_exists('logstore_lanalytics_log')) { $output->writeLine('Skipping cleanup of logstore_lanalytics_log: table does not exist.'); return; @@ -68,10 +127,10 @@ private function cleanupLAnalyticsLogs(OutputInterface $output) 'logstore_lanalytics_log', 'l', $sql, - ['cutoffdate' => $this->cutoffDate], + ['cutoffdate' => $this->cutoffdate], sprintf( 'Checking for logs to clean up (obsolete or older than %d days)...', - $this->cutoffDays + $this->cutoffdays ), $output ); diff --git a/classes/task/cleanup.php b/classes/task/cleanup.php index b5ee784..bcdf0b5 100644 --- a/classes/task/cleanup.php +++ b/classes/task/cleanup.php @@ -1,81 +1,178 @@ -db = $DB; - $this->dataRoot = $CFG->dataroot; - $this->backupTimeout = $CFG->cleanup_backup_timeout_days ?? FilesCheckout::DEFAULT_TIMEOUT_DAYS; - $this->draftTimeout = $CFG->cleanup_draft_timeout ?? FilesCheckout::DEFAULT_TIMEOUT_DAYS; - $this->logsTimeout = $CFG->cleanup_logs_timeout_days ?? LogsCleanup::DEFAULT_LIFETIME_DAYS; - $this->componentFilesDays = $CFG->cleanup_component_files_days ?? ComponentFilesCleanup::DEFAULT_LIFETIME_DAYS; - $this->gradesDays = $CFG->cleanup_grades_days ?? GradesCleanup::DEFAULT_LIFETIME_DAYS; - $this->courseModulesDays = $CFG->cleanup_course_modules_days ?? CourseModulesCleanup::DEFAULT_LIFETIME_DAYS; - $this->isAutoRemoveEnabled = (bool)$CFG->cleanup_run_autoremove ?? false; - $this->fs = get_file_storage(); - - $this->initializeSteps(); - } - - public function get_name() - { - return 'Database and disk clean-up'; - } - - public function execute() - { - $output = new MtraceOutput(); - - foreach ($this->steps as $step) { - $step->cleanUp($output); - } - } - - private function initializeSteps() - { - if ($this->isAutoRemoveEnabled) { - $this->steps[] = new CourseModulesCleanup($this->db, $this->courseModulesDays); - $this->steps[] = new GradesCleanup($this->db, $this->gradesDays); - $this->steps[] = new LogsCleanup($this->db, $this->logsTimeout); - $this->steps[] = new ComponentFilesCleanup($this->db, [ - 'assignsubmission_file', - 'backup', - ], $this->componentFilesDays); - $this->steps[] = new GhostFilesCleanup($this->db, $this->dataRoot); - } - - $this->steps[] = new FilesCheckout($this->db, $this->fs, $this->backupTimeout, $this->draftTimeout); - } -} +. + +namespace local_cleanup\task; + +use core\task\scheduled_task; +use file_storage; +use local_cleanup\output\MtraceOutput; +use local_cleanup\steps\CleanupStepInterface; +use local_cleanup\steps\ComponentFilesCleanup; +use local_cleanup\steps\CourseModulesCleanup; +use local_cleanup\steps\FilesCheckout; +use local_cleanup\steps\GhostFilesCleanup; +use local_cleanup\steps\GradesCleanup; +use local_cleanup\steps\LogsCleanup; +use moodle_database; + +/** + * Scheduled task for database and disk cleanup. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cleanup extends scheduled_task { + + /** + * Array of cleanup steps to execute. + * + * @var CleanupStepInterface[] + */ + private $steps = []; + + /** + * Database connection. + * + * @var moodle_database + */ + private $db; + + /** + * File storage instance. + * + * @var file_storage + */ + private $fs; + + /** + * Moodle data root directory path. + * + * @var string + */ + private $dataroot; + + /** + * Whether automatic removal is enabled. + * + * @var bool + */ + private $isautoremoveenabled; + + /** + * Number of days to keep backup files. + * + * @var int + */ + private $backuptimeout; + + /** + * Number of days to keep draft files. + * + * @var int + */ + private $drafttimeout; + + /** + * Number of days to keep logs. + * + * @var int + */ + private $logstimeout; + + /** + * Number of days to keep component files. + * + * @var int + */ + private $componentfilesdays; + + /** + * Number of days to keep grades. + * + * @var int + */ + private $gradesdays; + + /** + * Number of days to keep course modules. + * + * @var int + */ + private $coursemodulesdays; + + /** + * Constructor. + * + * Initializes the task with configuration from Moodle settings. + */ + public function __construct() { + global $DB, $CFG; + + $this->db = $DB; + $this->dataroot = $CFG->dataroot; + $this->backuptimeout = $CFG->cleanup_backup_timeout_days ?? FilesCheckout::DEFAULT_TIMEOUT_DAYS; + $this->drafttimeout = $CFG->cleanup_draft_timeout ?? FilesCheckout::DEFAULT_TIMEOUT_DAYS; + $this->logstimeout = $CFG->cleanup_logs_timeout_days ?? LogsCleanup::DEFAULT_LIFETIME_DAYS; + $this->componentfilesdays = $CFG->cleanup_component_files_days ?? ComponentFilesCleanup::DEFAULT_LIFETIME_DAYS; + $this->gradesdays = $CFG->cleanup_grades_days ?? GradesCleanup::DEFAULT_LIFETIME_DAYS; + $this->coursemodulesdays = $CFG->cleanup_course_modules_days ?? CourseModulesCleanup::DEFAULT_LIFETIME_DAYS; + $this->isautoremoveenabled = (bool)$CFG->cleanup_run_autoremove ?? false; + $this->fs = get_file_storage(); + + $this->initializeSteps(); + } + + /** + * Get the name of the task. + * + * @return string The name of the task + */ + public function get_name() { + return 'Database and disk clean-up'; + } + + /** + * Execute the task. + * + * Runs all configured cleanup steps. + */ + public function execute() { + $output = new MtraceOutput(); + + foreach ($this->steps as $step) { + $step->cleanUp($output); + } + } + + /** + * Initialize the cleanup steps based on configuration. + */ + private function initializesteps() { + if ($this->isautoremoveenabled) { + $this->steps[] = new CourseModulesCleanup($this->db, $this->coursemodulesdays); + $this->steps[] = new GradesCleanup($this->db, $this->gradesdays); + $this->steps[] = new LogsCleanup($this->db, $this->logstimeout); + $this->steps[] = new ComponentFilesCleanup($this->db, [ + 'assignsubmission_file', + 'backup', + ], $this->componentfilesdays); + $this->steps[] = new GhostFilesCleanup($this->db, $this->dataroot); + } + + $this->steps[] = new FilesCheckout($this->db, $this->fs, $this->backuptimeout, $this->drafttimeout); + } +} diff --git a/classes/task/scan.php b/classes/task/scan.php index 4835971..59ca9be 100644 --- a/classes/task/scan.php +++ b/classes/task/scan.php @@ -1,102 +1,158 @@ -db = $DB; - $this->data_root = $CFG->dataroot; - } - - public function get_name() - { - return 'Scan for unlinked files'; - } - - public function execute() - { - $sizeTotal = $this->scanRecursive('filedir'); - - mtrace(sprintf('Total found: %.3f GB', $sizeTotal / 1024 / 1024 / 1024)); - } - - private function scanRecursive(string $path, bool $printProgress = true): int - { - $size_total = 0; - $absolute = $this->data_root . DIRECTORY_SEPARATOR . $path; - $list = scandir($absolute); - - foreach ($list as $index => $item) { - if (preg_match('@^\.@', $item)) { - continue; - } - - $itemPath = $absolute . DIRECTORY_SEPARATOR . $item; - - if (is_dir($itemPath)) { - if ($printProgress) { - mtrace(sprintf( - 'Searching in "%s" (%d%%)...', - $itemPath, - ($index * 100) / count($list) - )); - } - - $size_total += $this->scanRecursive($path . DIRECTORY_SEPARATOR . $item, false); - - continue; - } - - $record = $this->db->get_record('files', ['contenthash' => $item], 'id', IGNORE_MULTIPLE); - - if (empty($record)) { - $size = filesize($itemPath); - $size_total += $size; - $mime = mime_content_type($itemPath); - - $this->insert($path . DIRECTORY_SEPARATOR . $item, $mime, $size); - - mtrace( - sprintf( - 'Record NOT found for file "%s", added for removal.', - $itemPath - ) - ); - } - } - - return $size_total; - } - - private function insert($path, $mime, $size) - { - $existing = $this->db->get_record('cleanup', ['path' => $path]); - - if (!empty($existing)) { - $existing->mime = $mime; - $existing->size = $size; - - $this->db->update_record('cleanup', $existing); - - return; - } - - $data = [ - 'path' => $path, - 'mime' => $mime, - 'size' => $size - ]; - - $this->db->insert_record('cleanup', (object)$data); - } -} +. + +namespace local_cleanup\task; + +use core\task\scheduled_task; +use moodle_database; + +/** + * Scheduled task for scanning unlinked files. + * + * Scans the file system for files that are not referenced in the database. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scan extends scheduled_task { + + /** + * Database connection. + * + * @var moodle_database + */ + private $db; + + /** + * Moodle data root directory path. + * + * @var string + */ + private $dataroot; + + /** + * Constructor. + */ + public function __construct() { + global $DB, $CFG; + + $this->db = $DB; + $this->dataroot = $CFG->dataroot; + } + + /** + * Get the name of the task. + * + * @return string The name of the task + */ + public function get_name() { + return 'Scan for unlinked files'; + } + + /** + * Execute the task. + * + * Scans for unlinked files and reports the total size found. + */ + public function execute() { + $sizetotal = $this->scanRecursive('filedir'); + + mtrace(sprintf('Total found: %.3f GB', $sizetotal / 1024 / 1024 / 1024)); + } + + /** + * Recursively scan a directory for unlinked files. + * + * @param string $path Relative path to scan + * @param bool $printprogress Whether to print progress information + * @return int Total size of unlinked files found in bytes + */ + private function scanrecursive(string $path, bool $printprogress = true): int { + $sizetotal = 0; + $absolute = $this->dataroot . DIRECTORY_SEPARATOR . $path; + $list = scandir($absolute); + + foreach ($list as $index => $item) { + if (preg_match('@^\.@', $item)) { + continue; + } + + $itempath = $absolute . DIRECTORY_SEPARATOR . $item; + + if (is_dir($itempath)) { + if ($printprogress) { + mtrace(sprintf( + 'Searching in "%s" (%d%%)...', + $itempath, + ($index * 100) / count($list) + )); + } + + $sizetotal += $this->scanRecursive($path . DIRECTORY_SEPARATOR . $item, false); + + continue; + } + + $record = $this->db->get_record('files', ['contenthash' => $item], 'id', IGNORE_MULTIPLE); + + if (empty($record)) { + $size = filesize($itempath); + $sizetotal += $size; + $mime = mime_content_type($itempath); + + $this->insert($path . DIRECTORY_SEPARATOR . $item, $mime, $size); + + mtrace( + sprintf( + 'Record NOT found for file "%s", added for removal.', + $itempath + ) + ); + } + } + + return $sizetotal; + } + + /** + * Insert or update a record in the local_cleanup_files table. + * + * @param string $path File path relative to dataroot + * @param string $mime MIME type of the file + * @param int $size Size of the file in bytes + */ + private function insert($path, $mime, $size) { + $existing = $this->db->get_record('local_cleanup_files', ['path' => $path]); + + if (!empty($existing)) { + $existing->mime = $mime; + $existing->size = $size; + + $this->db->update_record('local_cleanup_files', $existing); + + return; + } + + $data = [ + 'path' => $path, + 'mime' => $mime, + 'size' => $size, + ]; + + $this->db->insert_record('local_cleanup_files', (object)$data); + } +} diff --git a/cli/reinit_modules_cleanup.php b/cli/reinit_modules_cleanup.php index a035d41..90bd0d9 100644 --- a/cli/reinit_modules_cleanup.php +++ b/cli/reinit_modules_cleanup.php @@ -1,7 +1,27 @@ . + /** - * @global moodle_database $DB - * @global object $CFG + * CLI script to reinitialize course module cleanup tasks. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var moodle_database $DB + * @var object $CFG */ define('CLI_SCRIPT', true); @@ -30,13 +50,13 @@ mtrace("OK"); mtrace('Selecting courses with modules for removal... ', null); -$courses_ids = $DB->get_fieldset_sql("SELECT course FROM {course_modules} WHERE deletioninprogress = 1 GROUP BY course"); +$coursesids = $DB->get_fieldset_sql("SELECT course FROM {course_modules} WHERE deletioninprogress = 1 GROUP BY course"); mtrace("OK"); -foreach ($courses_ids as $id) { +foreach ($coursesids as $id) { mtrace("Selecting course modules for removal in course $id... ", null); - $course_modules = $DB->get_records( + $coursemodules = $DB->get_records( 'course_modules', ['course' => $id, 'deletioninprogress' => 1], '', @@ -44,11 +64,11 @@ ); $removaltask = new \core_course\task\course_delete_modules(); - $data = array( - 'cms' => $course_modules, + $data = [ + 'cms' => $coursemodules, 'userid' => $admin->id, 'realuserid' => $admin->id, - ); + ]; $removaltask->set_custom_data($data); \core\task\manager::queue_adhoc_task($removaltask); diff --git a/cli/usage_statistics.php b/cli/usage_statistics.php index d3748de..46ab43e 100644 --- a/cli/usage_statistics.php +++ b/cli/usage_statistics.php @@ -1,9 +1,27 @@ . + /** - * CLI script to display statistics about files and history tables - * - * @global moodle_database $DB - * @global object $CFG + * CLI script to display statistics about files and history tables. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var moodle_database $DB + * @var object $CFG */ define('CLI_SCRIPT', true); @@ -13,8 +31,9 @@ use local_cleanup\finder; /** - * Format file size with appropriate units - * + * Format file size with appropriate units. + * + * @package local_cleanup * @param int $bytes Size in bytes * @param int $decimals Number of decimal places * @return string Formatted size with unit @@ -39,7 +58,7 @@ function format_file_size($bytes, $decimals = 2) { 'backup' => false, ]; -foreach ($components as $component => $batch_removal) { +foreach ($components as $component => $batchremoval) { $stats = $finder->stats($component); mtrace(sprintf( @@ -49,26 +68,26 @@ function format_file_size($bytes, $decimals = 2) { format_file_size($stats->size) )); - // Calculate statistics for specific time periods + // Calculate statistics for specific time periods. $periods = [ - [null, '-1 year'], // From now to 1 year ago - ['-1 year', '-2 years'], // From 1 year ago to 2 years ago + [null, '-1 year'], // From now to 1 year ago. + ['-1 year', '-2 years'], // From 1 year ago to 2 years ago. ]; foreach ($periods as $index => $period) { list($from, $to) = $period; if ($from === null) { - // Files newer than 1 year - $stats_period = $finder->stats($component, $to, true); + // Files newer than 1 year. + $statsperiod = $finder->stats($component, $to, true); - $period_desc = sprintf("to %s", date('Y-m-d', strtotime($to))); + $perioddesc = sprintf("to %s", date('Y-m-d', strtotime($to))); } else { - // Files between 1 and 2 years old - // Use the new functionality to directly query for files in the specific time period - $stats_period = $finder->stats($component, $to, false, $from); + // Files between 1 and 2 years old. + // Use the new functionality to directly query for files in the specific time period. + $statsperiod = $finder->stats($component, $to, false, $from); - $period_desc = sprintf("from %s to %s", + $perioddesc = sprintf("from %s to %s", date('Y-m-d', strtotime($from)), date('Y-m-d', strtotime($to)) ); @@ -77,9 +96,9 @@ function format_file_size($bytes, $decimals = 2) { mtrace(sprintf( " %s (%s): %d files, %s", get_string($component, 'local_cleanup'), - $period_desc, - $stats_period->count, - format_file_size($stats_period->size) + $perioddesc, + $statsperiod->count, + format_file_size($statsperiod->size) )); } } @@ -89,10 +108,10 @@ function format_file_size($bytes, $decimals = 2) { 'logstore_standard_log' => 'timecreated', 'logstore_lanalytics_log' => 'timecreated', 'grade_grades_history' => 'timemodified', - 'grade_items_history' => 'timemodified' + 'grade_items_history' => 'timemodified', ]; -foreach ($tables as $table => $datetime_field) { +foreach ($tables as $table => $datetimefield) { if (!$DB->get_manager()->table_exists($table)) { mtrace("Table $table does not exist. Skipping."); continue; @@ -100,26 +119,51 @@ function format_file_size($bytes, $decimals = 2) { $count = $DB->count_records($table); - $size_query = $DB->get_records_sql("SHOW TABLE STATUS LIKE '{$CFG->prefix}{$table}'"); + // Get table size in a database-agnostic way. $size = 0; - foreach ($size_query as $info) { - // Handle case sensitivity in property names - $data_length = 0; - $index_length = 0; - - if (property_exists($info, 'Data_length')) { - $data_length = $info->Data_length; - } else if (property_exists($info, 'data_length')) { - $data_length = $info->data_length; - } - - if (property_exists($info, 'Index_length')) { - $index_length = $info->Index_length; - } else if (property_exists($info, 'index_length')) { - $index_length = $info->index_length; + try { + if ($CFG->dbtype === 'mysqli') { + // MySQL/MariaDB specific query. + $sizequery = $DB->get_records_sql("SHOW TABLE STATUS LIKE '{$CFG->prefix}{$table}'"); + foreach ($sizequery as $info) { + // Handle case sensitivity in property names. + $datalength = 0; + $indexlength = 0; + + if (property_exists($info, 'Data_length')) { + $datalength = $info->Data_length; + } else if (property_exists($info, 'data_length')) { + $datalength = $info->data_length; + } + + if (property_exists($info, 'Index_length')) { + $indexlength = $info->Index_length; + } else if (property_exists($info, 'index_length')) { + $indexlength = $info->index_length; + } + + $size = $datalength + $indexlength; + } + } else if ($CFG->dbtype === 'pgsql') { + // PostgreSQL specific query. + $sizequery = $DB->get_record_sql(" + SELECT pg_total_relation_size(schemaname||'.'||tablename) as total_size + FROM pg_tables + WHERE tablename = ? + ", [$CFG->prefix . $table]); + + if ($sizequery && isset($sizequery->total_size)) { + $size = (int)$sizequery->total_size; + } + } else { + // For other databases, estimate size based on record count. + // This is a rough approximation. + $size = $count * 1024; // Assume 1KB per record on average. } - - $size = $data_length + $index_length; + } catch (Exception $e) { + // If size calculation fails, just set to 0. + $size = 0; + mtrace("Could not calculate size for table $table: " . $e->getMessage()); } mtrace(sprintf( @@ -129,47 +173,47 @@ function format_file_size($bytes, $decimals = 2) { format_file_size($size) )); - // Calculate statistics for specific time periods + // Calculate statistics for specific time periods. $periods = [ - [null, '-1 year'], // From now to 1 year ago - ['-1 year', '-2 years'], // From 1 year ago to 2 years ago + [null, '-1 year'], // From now to 1 year ago. + ['-1 year', '-2 years'], // From 1 year ago to 2 years ago. ]; foreach ($periods as $period) { list($from, $to) = $period; if ($from === null) { - // Records newer than 1 year - $to_cutoff = strtotime($to); - $count_period = $DB->count_records_select($table, "$datetime_field >= ?", [$to_cutoff]); - $size_period = $count > 0 ? max(0, $size * ($count_period / $count)) : 0; - $period_desc = sprintf("to %s", date('Y-m-d', $to_cutoff)); + // Records newer than 1 year. + $tocutoff = strtotime($to); + $countperiod = $DB->count_records_select($table, "$datetimefield >= ?", [$tocutoff]); + $sizeperiod = $count > 0 ? max(0, $size * ($countperiod / $count)) : 0; + $perioddesc = sprintf("to %s", date('Y-m-d', $tocutoff)); } else { - // Records between 1 and 2 years old - $from_cutoff = strtotime($from); - $to_cutoff = strtotime($to); - - $count_period = $DB->count_records_select( - $table, - "$datetime_field >= ? AND $datetime_field < ?", - [$to_cutoff, $from_cutoff] + // Records between 1 and 2 years old. + $fromcutoff = strtotime($from); + $tocutoff = strtotime($to); + + $countperiod = $DB->count_records_select( + $table, + "$datetimefield >= ? AND $datetimefield < ?", + [$tocutoff, $fromcutoff] ); - $size_period = $count > 0 ? max(0, $size * ($count_period / $count)) : 0; + $sizeperiod = $count > 0 ? max(0, $size * ($countperiod / $count)) : 0; - $period_desc = sprintf("from %s to %s", - date('Y-m-d', $from_cutoff), - date('Y-m-d', $to_cutoff) + $perioddesc = sprintf("from %s to %s", + date('Y-m-d', $fromcutoff), + date('Y-m-d', $tocutoff) ); } mtrace(sprintf( " %s (%s): %d records, %s", $table, - $period_desc, - $count_period, - format_file_size($size_period) + $perioddesc, + $countperiod, + format_file_size($sizeperiod) )); } } diff --git a/db/install.xml b/db/install.xml new file mode 100644 index 0000000..e6c8129 --- /dev/null +++ b/db/install.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + +
+
+
diff --git a/db/tasks.php b/db/tasks.php index 5a091ae..c41b54e 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -1,22 +1,46 @@ - 'local_cleanup\task\scan', - 'blocking' => 0, - 'minute' => '0', - 'hour' => '5', - 'day' => '*', - 'dayofweek' => '1', - 'month' => '*', - ], - [ - 'classname' => 'local_cleanup\task\cleanup', - 'blocking' => 0, - 'minute' => '0', - 'hour' => '3', - 'day' => '*', - 'dayofweek' => '1', - 'month' => '*', - ], -]; +. + +/** + * Scheduled task definitions for local_cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = [ + [ + 'classname' => 'local_cleanup\task\scan', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '5', + 'day' => '*', + 'dayofweek' => '1', + 'month' => '*', + ], + [ + 'classname' => 'local_cleanup\task\cleanup', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '3', + 'day' => '*', + 'dayofweek' => '1', + 'month' => '*', + ], +]; diff --git a/db/upgrade.php b/db/upgrade.php index ef54c52..59b4817 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1,58 +1,66 @@ -get_manager(); - - if ($oldversion < 2020020701) { - $cleanup_table = new xmldb_table('cleanup'); - $cleanup_table->add_field( - 'id', - XMLDB_TYPE_INTEGER, - '10', - true, - XMLDB_NOTNULL, - XMLDB_SEQUENCE - ); - - $cleanup_table->add_field('path', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL); - $cleanup_table->add_field('mime', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL); - $cleanup_table->add_field('size', XMLDB_TYPE_INTEGER, '10', true, XMLDB_NOTNULL); - - $primary = new xmldb_key('primary'); - $primary->set_attributes(XMLDB_KEY_PRIMARY, ['id']); - $cleanup_table->addKey($primary); - - $manager->create_table($cleanup_table); - } - - if ($oldversion < 2023061000) { - $table = new xmldb_table('files'); - $manager->add_index( - $table, - new xmldb_index('component', XMLDB_INDEX_NOTUNIQUE, ['component']) - ); - $manager->add_index( - $table, - new xmldb_index('component_filesize', XMLDB_INDEX_NOTUNIQUE, ['component', 'filesize']) - ); - $manager->add_index( - $table, - new xmldb_index('component_timecreated', XMLDB_INDEX_NOTUNIQUE, ['component', 'timecreated']) - ); - } - - return true; -} +. + +/** + * Database upgrade script for local_cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @param int $oldversion The old version number + * + * @return bool Success status + * + * @throws coding_exception + * @throws ddl_exception + */ +function xmldb_local_cleanup_upgrade($oldversion = 0) { + global $DB; + + $manager = $DB->get_manager(); + + if ($oldversion < 2023061000) { + $table = new xmldb_table('files'); + $manager->add_index( + $table, + new xmldb_index('component', XMLDB_INDEX_NOTUNIQUE, ['component']) + ); + $manager->add_index( + $table, + new xmldb_index('component_filesize', XMLDB_INDEX_NOTUNIQUE, ['component', 'filesize']) + ); + $manager->add_index( + $table, + new xmldb_index('component_timecreated', XMLDB_INDEX_NOTUNIQUE, ['component', 'timecreated']) + ); + + upgrade_plugin_savepoint(true, 2023061000, 'local', 'cleanup'); + } + + if ($oldversion < 2025080700) { + $oldtable = new xmldb_table('cleanup'); + $newtable = new xmldb_table('local_cleanup_files'); + + if ($manager->table_exists($oldtable) && !$manager->table_exists($newtable)) { + $manager->rename_table($oldtable, 'local_cleanup_files'); + } + + upgrade_plugin_savepoint(true, 2025080700, 'local', 'cleanup'); + } + + return true; +} diff --git a/download.php b/download.php index c069cb2..9933928 100644 --- a/download.php +++ b/download.php @@ -1,43 +1,63 @@ -libdir . '/filelib.php'); - -require_login(); - -if (!is_siteadmin()) { - header('HTTP/1.1 403 Forbidden'); - exit('Forbidden!'); -} - -$file_path = optional_param('path', 0, PARAM_TEXT); -$file_id = optional_param('id', 0, PARAM_INT); - -if (!empty($file_path)) { - $absolute = $CFG->dataroot . DIRECTORY_SEPARATOR . $file_path; - - if (!is_readable($absolute)) { - header('HTTP/1.1 404 Not found'); - exit('Not found!'); - } - - send_file($absolute, basename($absolute)); -} - -$file = $DB->get_record('files', ['id' => $file_id], '*', MUST_EXIST); - -$url = moodle_url::make_pluginfile_url( - $file->contextid, - $file->component, - $file->filearea, - $file->itemid, - $file->filepath, - $file->filename, - true -); - -redirect($url, '', 0); +. + +/** + * File download handler for the cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var moodle_database $DB + * @var stdClass $CFG + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/filelib.php'); + +require_login(); + +if (!is_siteadmin()) { + header('HTTP/1.1 403 Forbidden'); + exit('Forbidden!'); +} + +$filepath = optional_param('path', 0, PARAM_TEXT); +$fileid = optional_param('id', 0, PARAM_INT); + +if (!empty($filepath)) { + $absolute = $CFG->dataroot . DIRECTORY_SEPARATOR . $filepath; + + if (!is_readable($absolute)) { + header('HTTP/1.1 404 Not found'); + exit('Not found!'); + } + + send_file($absolute, basename($absolute)); +} + +$file = $DB->get_record('files', ['id' => $fileid], '*', MUST_EXIST); + +$url = moodle_url::make_pluginfile_url( + $file->contextid, + $file->component, + $file->filearea, + $file->itemid, + $file->filepath, + $file->filename, + true +); + +redirect($url, '', 0); diff --git a/files.php b/files.php index 19cb675..ff79df1 100644 --- a/files.php +++ b/files.php @@ -1,146 +1,166 @@ -libdir . '/formslib.php'); - -use local_cleanup\finder; -use local_cleanup\form\filter_form; - -$PAGE->set_context(context_system::instance()); -$PAGE->set_url('/local/cleanup/files.php'); -$PAGE->set_title(get_string('files')); -$PAGE->set_heading(get_string('files')); -$PAGE->set_pagelayout('admin'); - -require_login(); - -if (!is_siteadmin()) { - header('HTTP/1.1 403 Forbidden'); - exit('Forbidden!'); -} - -$page = optional_param('page', 0, PARAM_INT); -$limit = $CFG->cleanup_items_per_page ?? finder::LIMIT_DEFAULT; - -$filter = [ - 'filesize' => optional_param('filesize', 50, PARAM_INT), - 'name_like' => optional_param('name_like', '', PARAM_TEXT), - 'user_like' => optional_param('user_like', '', PARAM_TEXT), - 'component' => optional_param('component', '', PARAM_TEXT), - 'user_deleted' => optional_param('user_deleted', '', PARAM_TEXT), -]; - -$filter_form = new filter_form(null, $filter); - -if ($filter_form->is_cancelled()) { - redirect($PAGE->url); -} - -$redirect_url = new moodle_url($PAGE->url, array_merge($filter, ['page' => $page])); - -$finder = new finder($DB); -$items = $finder->find($limit, $page * $limit, $filter); -$total_items = $finder->count($filter); -$max_items = pow(10, 3) * ($page + 1); - -$table = new html_table(); -$table->head = [ - get_string('filename', 'backup'), - get_string('component', 'cache'), - get_string('size'), - get_string('user', 'admin'), - get_string('date'), - '' -]; - -$table->size = ['30%', '15%', '10%', '30%', '15%', '1%']; - -while ($items->valid()) { - $item = $items->current(); - - $actions = [ - html_writer::link( - new moodle_url('/local/cleanup/download.php', ['id' => $item->id]), - $OUTPUT->pix_icon('i/down', get_string('download')) - ), - ]; - - if ( - preg_match('/^mod_/', $item->component) - || ($item->component === 'backup' && $item->filearea === 'course') - ) { - array_unshift( - $actions, - html_writer::link( - new moodle_url('/local/cleanup/open.php', ['id' => $item->id]), - $OUTPUT->pix_icon('i/preview', get_string('view')), - [ - 'target' => '_blank' - ] - ) - ); - } - - $actions[] = html_writer::link( - new moodle_url('/local/cleanup/remove.php', ['id' => $item->id, 'redirect' => $redirect_url]), - $OUTPUT->pix_icon('t/delete', get_string('delete')) - ); - - if (!$item->user_deleted) { - $user = html_writer::link( - new moodle_url('/user/profile.php', ['id' => $item->userid]), - fullname($item), - [ - 'target' => '_blank' - ] - ); - } else { - $user = html_writer::tag('del', fullname($item)); - } - - $table->data[] = [ - $item->filename, - sprintf('%s, %s', $item->component, $item->filearea), - sprintf( - '%.1f %s', - $item->filesize / pow(1024, 2), - get_string('sizemb') - ), - $user, - date('Y-m-d H:i', $item->timecreated), - implode(' ', $actions) - ]; - - $items->next(); -} - -$pagination = $OUTPUT->paging_bar( - $total_items > $max_items ? $max_items : $total_items, - $page, - $limit, - new moodle_url($PAGE->url, $filter) -); - -echo $OUTPUT->header(); - -$filter_form->display(); - -if (count($table->data) !== 0) { - echo html_writer::tag( - 'p', - get_string('files_total', 'local_cleanup') . ': ' . $total_items - ); - echo html_writer::table($table); -} else { - echo $OUTPUT->notification(get_string('nothingtoshow', 'local_cleanup')); -} - -echo $OUTPUT->box($pagination, 'text-center'); -echo $OUTPUT->footer(); +. + +/** + * Files management page for cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var moodle_page $PAGE + * @var moodle_database $DB + * @var stdClass $USER + * @var stdClass $CFG + * @var renderer_base $OUTPUT + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/formslib.php'); + +use local_cleanup\finder; +use local_cleanup\form\filter_form; + +$PAGE->set_context(context_system::instance()); +$PAGE->set_url('/local/cleanup/files.php'); +$PAGE->set_title(get_string('files')); +$PAGE->set_heading(get_string('files')); +$PAGE->set_pagelayout('admin'); + +require_login(); + +if (!is_siteadmin()) { + header('HTTP/1.1 403 Forbidden'); + exit('Forbidden!'); +} + +$page = optional_param('page', 0, PARAM_INT); +$limit = $CFG->cleanup_items_per_page ?? finder::LIMIT_DEFAULT; + +$filter = [ + 'filesize' => optional_param('filesize', 50, PARAM_INT), + 'name_like' => optional_param('name_like', '', PARAM_TEXT), + 'user_like' => optional_param('user_like', '', PARAM_TEXT), + 'component' => optional_param('component', '', PARAM_TEXT), + 'user_deleted' => optional_param('user_deleted', '', PARAM_TEXT), +]; + +$filterform = new filter_form(null, $filter); + +if ($filterform->is_cancelled()) { + redirect($PAGE->url); +} + +$redirecturl = new moodle_url($PAGE->url, array_merge($filter, ['page' => $page])); + +$finder = new finder($DB); +$items = $finder->find($limit, $page * $limit, $filter); +$totalitems = $finder->count($filter); +$maxitems = pow(10, 3) * ($page + 1); + +$table = new html_table(); +$table->head = [ + get_string('filename', 'backup'), + get_string('component', 'cache'), + get_string('size'), + get_string('user', 'admin'), + get_string('date'), + '', +]; + +$table->size = ['30%', '15%', '10%', '30%', '15%', '1%']; + +while ($items->valid()) { + $item = $items->current(); + + $actions = [ + html_writer::link( + new moodle_url('/local/cleanup/download.php', ['id' => $item->id]), + $OUTPUT->pix_icon('i/down', get_string('download')) + ), + ]; + + if ( + preg_match('/^mod_/', $item->component) + || ($item->component === 'backup' && $item->filearea === 'course') + ) { + array_unshift( + $actions, + html_writer::link( + new moodle_url('/local/cleanup/open.php', ['id' => $item->id]), + $OUTPUT->pix_icon('i/preview', get_string('view')), + [ + 'target' => '_blank', + ] + ) + ); + } + + $actions[] = html_writer::link( + new moodle_url('/local/cleanup/remove.php', ['id' => $item->id, 'redirect' => $redirecturl]), + $OUTPUT->pix_icon('t/delete', get_string('delete')) + ); + + if (!$item->user_deleted) { + $user = html_writer::link( + new moodle_url('/user/profile.php', ['id' => $item->userid]), + fullname($item), + [ + 'target' => '_blank', + ] + ); + } else { + $user = html_writer::tag('del', fullname($item)); + } + + $table->data[] = [ + $item->filename, + sprintf('%s, %s', $item->component, $item->filearea), + sprintf( + '%.1f %s', + $item->filesize / pow(1024, 2), + get_string('sizemb') + ), + $user, + date('Y-m-d H:i', $item->timecreated), + implode(' ', $actions), + ]; + + $items->next(); +} + +$pagination = $OUTPUT->paging_bar( + $totalitems > $maxitems ? $maxitems : $totalitems, + $page, + $limit, + new moodle_url($PAGE->url, $filter) +); + +echo $OUTPUT->header(); + +$filterform->display(); + +if (count($table->data) !== 0) { + echo html_writer::tag( + 'p', + get_string('files_total', 'local_cleanup') . ': ' . $totalitems + ); + echo html_writer::table($table); +} else { + echo $OUTPUT->notification(get_string('nothingtoshow', 'local_cleanup')); +} + +echo $OUTPUT->box($pagination, 'text-center'); +echo $OUTPUT->footer(); diff --git a/ghost.php b/ghost.php index 31694b2..94aa43d 100644 --- a/ghost.php +++ b/ghost.php @@ -1,95 +1,115 @@ -set_context(context_system::instance()); -$PAGE->set_url('/local/cleanup/ghost.php'); -$PAGE->set_title(get_string('ghostfiles', 'local_cleanup')); -$PAGE->set_heading(get_string('ghostfiles', 'local_cleanup')); -$PAGE->set_pagelayout('admin'); - -require_login(); - -if (!is_siteadmin()) { - header('HTTP/1.1 403 Forbidden'); - exit('Forbidden!'); -} - -$task = task_manager::get_scheduled_task(cleanup::class); -$page = optional_param('page', 0, PARAM_INT); -$limit = 250; - -$items = $DB->get_recordset('cleanup', [], 'size DESC', '*', $page * $limit, $limit); -$total_items = $DB->count_records('cleanup'); -$total_size = $DB->get_field('cleanup', 'SUM(size)', []); - -$table = new html_table(); -$table->head = [ - get_string('file'), - 'MIME', - get_string('size'), - '', -]; - -while ($items->valid()) { - $item = $items->current(); - - $actions = [ - html_writer::link( - new moodle_url('/local/cleanup/download.php', ['path' => $item->path]), - $OUTPUT->pix_icon('i/down', get_string('download')) - ), - ]; - - $table->data[] = [ - $item->path, - $item->mime, - sprintf( - '%.1f %s', - $item->size / pow(1024, 2), - get_string('sizemb') - ), - implode(' ', $actions) - ]; - - $items->next(); -} - -$pagination = $OUTPUT->paging_bar($total_items, $page, $limit, $PAGE->url); - -echo $OUTPUT->header(); - -echo $OUTPUT->box( - html_writer::tag('p', - html_writer::tag('b', - get_string( - 'ghosttotalheader', - 'local_cleanup', - [ - 'files' => $total_items, - 'size' => sprintf('%.3f', $total_size / pow(1024, 3)), - 'cleanup_date' => date(DATE_ISO8601, $task->get_next_run_time()), - ] - ) - ) - ) -); - -if (count($table->data) !== 0) { - echo html_writer::table($table); -} else { - echo $OUTPUT->notification(get_string('nothingtoshow', 'local_cleanup'), 'notifysuccess'); -} - -echo $OUTPUT->box($pagination, 'text-center'); -echo $OUTPUT->footer(); +. + +/** + * Ghost files management page for cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var moodle_page $PAGE + * @var moodle_database $DB + * @var stdClass $USER + * @var stdClass $CFG + * @var renderer_base $OUTPUT + */ + +require_once(__DIR__ . '/../../config.php'); + +use core\task\manager as task_manager; +use local_cleanup\task\cleanup; + +$PAGE->set_context(context_system::instance()); +$PAGE->set_url('/local/cleanup/ghost.php'); +$PAGE->set_title(get_string('ghostfiles', 'local_cleanup')); +$PAGE->set_heading(get_string('ghostfiles', 'local_cleanup')); +$PAGE->set_pagelayout('admin'); + +require_login(); + +if (!is_siteadmin()) { + header('HTTP/1.1 403 Forbidden'); + exit('Forbidden!'); +} + +$task = task_manager::get_scheduled_task(cleanup::class); +$page = optional_param('page', 0, PARAM_INT); +$limit = 250; + +$items = $DB->get_recordset('local_cleanup_files', [], 'size DESC', '*', $page * $limit, $limit); +$totalitems = $DB->count_records('local_cleanup_files'); +$totalsize = $DB->get_field('local_cleanup_files', 'SUM(size)', []); + +$table = new html_table(); +$table->head = [ + get_string('file'), + 'MIME', + get_string('size'), + '', +]; + +while ($items->valid()) { + $item = $items->current(); + + $actions = [ + html_writer::link( + new moodle_url('/local/cleanup/download.php', ['path' => $item->path]), + $OUTPUT->pix_icon('i/down', get_string('download')) + ), + ]; + + $table->data[] = [ + $item->path, + $item->mime, + sprintf( + '%.1f %s', + $item->size / pow(1024, 2), + get_string('sizemb') + ), + implode(' ', $actions), + ]; + + $items->next(); +} + +$pagination = $OUTPUT->paging_bar($totalitems, $page, $limit, $PAGE->url); + +echo $OUTPUT->header(); + +echo $OUTPUT->box( + html_writer::tag('p', + html_writer::tag('b', + get_string( + 'ghosttotalheader', + 'local_cleanup', + [ + 'files' => $totalitems, + 'size' => sprintf('%.3f', $totalsize / pow(1024, 3)), + 'cleanup_date' => date(DATE_ISO8601, $task->get_next_run_time()), + ] + ) + ) + ) +); + +if (count($table->data) !== 0) { + echo html_writer::table($table); +} else { + echo $OUTPUT->notification(get_string('nothingtoshow', 'local_cleanup'), 'notifysuccess'); +} + +echo $OUTPUT->box($pagination, 'text-center'); +echo $OUTPUT->footer(); diff --git a/lang/en/local_cleanup.php b/lang/en/local_cleanup.php index 4bfe6e7..b36f174 100644 --- a/lang/en/local_cleanup.php +++ b/lang/en/local_cleanup.php @@ -1,35 +1,58 @@ -files}, total size: {$a->size}Gb, next clean-up: {$a->cleanup_date}'; -$string['nothingtoshow'] = 'Nothing to show'; -$string['removeconfirm'] = 'You about to remove file "{$a->name}" with id "{$a->id}". Are you sure?'; -$string['fileremoved'] = 'File "{$a->name}" removed, {$a->size}Mb cleaned'; -$string['failtoremove'] = 'Failed to remove file "{$a->name}"'; -$string['settingspage'] = 'Clean-up settings'; -$string['itemsperpage'] = 'Items per page'; -$string['itemsperpagedesc'] = 'Affects performance'; -$string['backuplifetime'] = 'Backup files lifetime'; -$string['backuplifetimedesc'] = 'Number of days to keep backups'; -$string['draftlifetime'] = 'Draft files lifetime'; -$string['draftlifetimedesc'] = 'Number of days to keep draft files'; -$string['logslifetime'] = 'Logs lifetime'; -$string['logslifetimedesc'] = 'Number of days to keep logs'; -$string['componentfileslifetime'] = 'Component files lifetime'; -$string['componentfileslifetimedesc'] = 'Number of days to keep component files'; -$string['gradeslifetime'] = 'Grades history lifetime'; -$string['gradeslifetimedesc'] = 'Number of days to keep grades history'; -$string['directorylifetime'] = 'Directory files lifetime'; -$string['directorylifetimedesc'] = 'Number of days to keep directory files'; -$string['coursemoduleslifetime'] = 'Course modules lifetime'; -$string['coursemoduleslifetimedesc'] = 'Number of days to keep orphaned course modules'; -$string['autoremove'] = 'Auto remove outdated files'; -$string['autoremovedesc'] = 'Remove outdated files found in the filesystem on clean-up'; -$string['files_total'] = 'Files total'; -$string['assignsubmission_file'] = 'Uploaded students\' submissions'; -$string['backup'] = 'Backup copies'; -$string['batchremovaldone'] = 'Batch removal completed'; +. + +/** + * English language strings for local_cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['assignsubmission_file'] = 'Uploaded students\' submissions'; +$string['autoremove'] = 'Auto remove outdated files'; +$string['autoremovedesc'] = 'Remove outdated files found in the filesystem on clean-up'; +$string['backup'] = 'Backup copies'; +$string['backuplifetime'] = 'Backup files lifetime'; +$string['backuplifetimedesc'] = 'Number of days to keep backups'; +$string['batchremovaldone'] = 'Batch removal completed'; +$string['componentfileslifetime'] = 'Component files lifetime'; +$string['componentfileslifetimedesc'] = 'Number of days to keep component files'; +$string['coursemoduleslifetime'] = 'Course modules lifetime'; +$string['coursemoduleslifetimedesc'] = 'Number of days to keep orphaned course modules'; +$string['directorylifetime'] = 'Directory files lifetime'; +$string['directorylifetimedesc'] = 'Number of days to keep directory files'; +$string['draftlifetime'] = 'Draft files lifetime'; +$string['draftlifetimedesc'] = 'Number of days to keep draft files'; +$string['failtoremove'] = 'Failed to remove file "{$a->name}"'; +$string['fileremoved'] = 'File "{$a->name}" removed, {$a->size}Mb cleaned'; +$string['files_total'] = 'Files total'; +$string['ghostfiles'] = 'Unlinked files'; +$string['ghosttotalheader'] = 'Total files found: {$a->files}, total size: {$a->size}Gb, next clean-up: {$a->cleanup_date}'; +$string['gradeslifetime'] = 'Grades history lifetime'; +$string['gradeslifetimedesc'] = 'Number of days to keep grades history'; +$string['itemsperpage'] = 'Items per page'; +$string['itemsperpagedesc'] = 'Affects performance'; +$string['logslifetime'] = 'Logs lifetime'; +$string['logslifetimedesc'] = 'Number of days to keep logs'; +$string['nothingtoshow'] = 'Nothing to show'; +$string['pluginname'] = 'Clean-up'; +$string['removeconfirm'] = 'You about to remove file "{$a->name}" with id "{$a->id}". Are you sure?'; +$string['settingspage'] = 'Clean-up settings'; +$string['title'] = 'Clean-up'; +$string['userfiles'] = 'Users files'; diff --git a/open.php b/open.php index 37a75d2..9c4c688 100644 --- a/open.php +++ b/open.php @@ -1,53 +1,73 @@ -get_record('files', ['id' => $id], '*', MUST_EXIST); - -if ($file->component === 'backup' && $file->filearea === 'course') { - $url = new moodle_url('/backup/restorefile.php', ['contextid' => $file->contextid]); - - redirect($url); -} - -$context = $DB->get_record('context', ['id' => $file->contextid], '*', MUST_EXIST); - -if (CONTEXT_MODULE === (int)$context->contextlevel) { - $module = $DB->get_record('course_modules', ['id' => $context->instanceid], '*', MUST_EXIST); - - if ($file->component === 'mod_resource') { - $url = sprintf( - '%s#module-%d', - new moodle_url('/course/view.php', ['id' => $module->course]), - $module->id - ); - - redirect($url); - } - - redirect( - new moodle_url( - sprintf( - '/mod/%s/view.php', - str_replace('mod_', '', $file->component) - ), - [ - 'id' => $module->id - ] - ) - ); -} - -throw new moodle_exception('unknowncontext', 'local_cleanup'); +. + +/** + * File viewer for the cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var moodle_database $DB + */ + +require_once(__DIR__ . '/../../config.php'); + +require_login(); + +if (!is_siteadmin()) { + header('HTTP/1.1 403 Forbidden'); + exit('Forbidden!'); +} + +$id = optional_param('id', 0, PARAM_INT); + +$file = $DB->get_record('files', ['id' => $id], '*', MUST_EXIST); + +if ($file->component === 'backup' && $file->filearea === 'course') { + $url = new moodle_url('/backup/restorefile.php', ['contextid' => $file->contextid]); + + redirect($url); +} + +$context = $DB->get_record('context', ['id' => $file->contextid], '*', MUST_EXIST); + +if (CONTEXT_MODULE === (int)$context->contextlevel) { + $module = $DB->get_record('course_modules', ['id' => $context->instanceid], '*', MUST_EXIST); + + if ($file->component === 'mod_resource') { + $url = sprintf( + '%s#module-%d', + new moodle_url('/course/view.php', ['id' => $module->course]), + $module->id + ); + + redirect($url); + } + + redirect( + new moodle_url( + sprintf( + '/mod/%s/view.php', + str_replace('mod_', '', $file->component) + ), + [ + 'id' => $module->id, + ] + ) + ); +} + +throw new moodle_exception('unknowncontext', 'local_cleanup'); diff --git a/remove.php b/remove.php index ee40550..26fdd14 100644 --- a/remove.php +++ b/remove.php @@ -1,90 +1,111 @@ -libdir . '/formslib.php'); - -use core\notification; - -$PAGE->set_context(context_system::instance()); -$PAGE->set_url('/local/cleanup/remove.php'); -$PAGE->set_title(get_string('remove')); -$PAGE->set_heading(get_string('remove')); -$PAGE->set_pagelayout('default'); - -require_login(); - -if (!is_siteadmin()) { - header('HTTP/1.1 403 Forbidden'); - exit('Forbidden!'); -} - -$id = optional_param('id', 0, PARAM_INT); -$file = $DB->get_record('files', ['id' => $id], '*', MUST_EXIST); - -$redirect_url = new moodle_url(optional_param('redirect', '/local/cleanup/files.php', PARAM_TEXT)); - -if (optional_param('confirm', false, PARAM_BOOL)) { - $fs = get_file_storage(); - $file = $fs->get_file_instance($file); - - $resource = $fs->get_file_system()->get_content_file_handle($file); - $message = get_string( - 'fileremoved', - 'local_cleanup', - [ - 'name' => $file->get_filename(), - 'size' => $file->get_filesize() / 1024 / 1024, - ] - ); - $message_type = notification::SUCCESS; - - if (!$resource) { - // looks like the file is missing, so just removing the record. - $DB->delete_records('files', ['contenthash' => $file->get_contenthash()]); - } else { - $uri = stream_get_meta_data($resource)['uri']; - fclose($resource); - - if (unlink($uri)) { - $DB->delete_records('files', ['contenthash' => $file->get_contenthash()]); - } else { - $message = get_string( - 'failtoremove', - 'local_cleanup', - [ - 'name' => $file->get_filename() - ] - ); - $message_type = notification::ERROR; - } - } - - redirect($redirect_url, $message, 3, $message_type); -} - -echo $OUTPUT->header(); - -echo $OUTPUT->confirm( - sprintf( - '%s %s %s, %s %s?', - get_string('remove'), - mb_strtolower(get_string('file')), - $file->filename, - round($file->filesize / 1024 / 1024, 2), - get_string('sizemb') - ), - new moodle_url($PAGE->url, [ - 'id' => $id, - 'confirm' => 1, - ]), - $redirect_url -); - -echo $OUTPUT->footer(); +. + +/** + * File removal handler for the cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @phpcs:ignore moodle.Commenting.ValidTags.Invalid + * @var stdClass $CFG + * @var stdClass $USER + * @var moodle_page $PAGE + * @var moodle_database $DB + * @var renderer_base $OUTPUT + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/formslib.php'); + +use core\notification; + +$PAGE->set_context(context_system::instance()); +$PAGE->set_url('/local/cleanup/remove.php'); +$PAGE->set_title(get_string('remove')); +$PAGE->set_heading(get_string('remove')); +$PAGE->set_pagelayout('default'); + +require_login(); + +if (!is_siteadmin()) { + header('HTTP/1.1 403 Forbidden'); + exit('Forbidden!'); +} + +$id = optional_param('id', 0, PARAM_INT); +$file = $DB->get_record('files', ['id' => $id], '*', MUST_EXIST); + +$redirecturl = new moodle_url(optional_param('redirect', '/local/cleanup/files.php', PARAM_TEXT)); + +if (optional_param('confirm', false, PARAM_BOOL)) { + $fs = get_file_storage(); + $file = $fs->get_file_instance($file); + + $resource = $fs->get_file_system()->get_content_file_handle($file); + $message = get_string( + 'fileremoved', + 'local_cleanup', + [ + 'name' => $file->get_filename(), + 'size' => $file->get_filesize() / 1024 / 1024, + ] + ); + $messagetype = notification::SUCCESS; + + if (!$resource) { + // Looks like the file is missing, so just removing the record. + $DB->delete_records('files', ['contenthash' => $file->get_contenthash()]); + } else { + $uri = stream_get_meta_data($resource)['uri']; + fclose($resource); + + if (unlink($uri)) { + $DB->delete_records('files', ['contenthash' => $file->get_contenthash()]); + } else { + $message = get_string( + 'failtoremove', + 'local_cleanup', + [ + 'name' => $file->get_filename(), + ] + ); + $messagetype = notification::ERROR; + } + } + + redirect($redirecturl, $message, 3, $messagetype); +} + +echo $OUTPUT->header(); + +echo $OUTPUT->confirm( + sprintf( + '%s %s %s, %s %s?', + get_string('remove'), + mb_strtolower(get_string('file')), + $file->filename, + round($file->filesize / 1024 / 1024, 2), + get_string('sizemb') + ), + new moodle_url($PAGE->url, [ + 'id' => $id, + 'confirm' => 1, + ]), + $redirecturl +); + +echo $OUTPUT->footer(); diff --git a/settings.php b/settings.php index 9443c17..35833fc 100644 --- a/settings.php +++ b/settings.php @@ -1,117 +1,137 @@ -add( - 'root', - new admin_category('local_cleanup', get_string('pluginname', 'local_cleanup')) - ); - - $ADMIN->add( - 'local_cleanup', - new admin_externalpage( - 'local_cleanup_userfiles', - get_string('files'), - new moodle_url('/local/cleanup/files.php') - ) - ); - - $ADMIN->add( - 'local_cleanup', - new admin_externalpage( - 'local_cleanup_ghostfiles', - get_string('ghostfiles', 'local_cleanup'), - new moodle_url('/local/cleanup/ghost.php') - ) - ); - - $settings = new admin_settingpage( - 'local_cleanup_admin', - get_string('settingspage', 'local_cleanup') - ); - $ADMIN->add('localplugins', $settings); - - $settings->add( - new admin_setting_configtext( - 'cleanup_items_per_page', - get_string('itemsperpage', 'local_cleanup'), - get_string('itemsperpagedesc', 'local_cleanup'), - local_cleanup\finder::LIMIT_DEFAULT, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configtext( - 'cleanup_backup_timeout_days', - get_string('backuplifetime', 'local_cleanup'), - get_string('backuplifetimedesc', 'local_cleanup'), - 30, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configtext( - 'cleanup_draft_timeout', - get_string('draftlifetime', 'local_cleanup'), - get_string('draftlifetimedesc', 'local_cleanup'), - 30, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configtext( - 'cleanup_logs_timeout_days', - get_string('logslifetime', 'local_cleanup'), - get_string('logslifetimedesc', 'local_cleanup'), - 500, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configtext( - 'cleanup_component_files_days', - get_string('componentfileslifetime', 'local_cleanup'), - get_string('componentfileslifetimedesc', 'local_cleanup'), - 180, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configtext( - 'cleanup_grades_days', - get_string('gradeslifetime', 'local_cleanup'), - get_string('gradeslifetimedesc', 'local_cleanup'), - 500, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configtext( - 'cleanup_course_modules_days', - get_string('coursemoduleslifetime', 'local_cleanup'), - get_string('coursemoduleslifetimedesc', 'local_cleanup'), - 7, - PARAM_INT - ) - ); - - $settings->add( - new admin_setting_configcheckbox( - 'cleanup_run_autoremove', - get_string('autoremove', 'local_cleanup'), - get_string('autoremovedesc', 'local_cleanup'), - 0 //disabled by default. - ) - ); -} +. + +/** + * Settings for the local cleanup plugin. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @var bool $hassiteconfig + * @var admin_root $ADMIN + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { + $ADMIN->add( + 'root', + new admin_category('local_cleanup', get_string('pluginname', 'local_cleanup')) + ); + + $ADMIN->add( + 'local_cleanup', + new admin_externalpage( + 'local_cleanup_userfiles', + get_string('files'), + new moodle_url('/local/cleanup/files.php') + ) + ); + + $ADMIN->add( + 'local_cleanup', + new admin_externalpage( + 'local_cleanup_ghostfiles', + get_string('ghostfiles', 'local_cleanup'), + new moodle_url('/local/cleanup/ghost.php') + ) + ); + + $settings = new admin_settingpage( + 'local_cleanup_admin', + get_string('settingspage', 'local_cleanup') + ); + $ADMIN->add('localplugins', $settings); + + $settings->add( + new admin_setting_configtext( + 'cleanup_items_per_page', + get_string('itemsperpage', 'local_cleanup'), + get_string('itemsperpagedesc', 'local_cleanup'), + local_cleanup\finder::LIMIT_DEFAULT, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configtext( + 'cleanup_backup_timeout_days', + get_string('backuplifetime', 'local_cleanup'), + get_string('backuplifetimedesc', 'local_cleanup'), + 30, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configtext( + 'cleanup_draft_timeout', + get_string('draftlifetime', 'local_cleanup'), + get_string('draftlifetimedesc', 'local_cleanup'), + 30, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configtext( + 'cleanup_logs_timeout_days', + get_string('logslifetime', 'local_cleanup'), + get_string('logslifetimedesc', 'local_cleanup'), + 500, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configtext( + 'cleanup_component_files_days', + get_string('componentfileslifetime', 'local_cleanup'), + get_string('componentfileslifetimedesc', 'local_cleanup'), + 180, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configtext( + 'cleanup_grades_days', + get_string('gradeslifetime', 'local_cleanup'), + get_string('gradeslifetimedesc', 'local_cleanup'), + 500, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configtext( + 'cleanup_course_modules_days', + get_string('coursemoduleslifetime', 'local_cleanup'), + get_string('coursemoduleslifetimedesc', 'local_cleanup'), + 7, + PARAM_INT + ) + ); + + $settings->add( + new admin_setting_configcheckbox( + 'cleanup_run_autoremove', + get_string('autoremove', 'local_cleanup'), + get_string('autoremovedesc', 'local_cleanup'), + 0 // Disabled by default. + ) + ); +} diff --git a/version.php b/version.php index eb033f4..9468d34 100644 --- a/version.php +++ b/version.php @@ -15,16 +15,21 @@ // along with Moodle. If not, see . /** - * @author Yevhen Matasar + * Plugin version and other meta-data are defined here. + * + * @package local_cleanup + * @copyright 2024 Grinchenko University + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yevhen Matasar * * @var $plugin stdClass */ -defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); $plugin->component = 'local_cleanup'; -$plugin->version = 2025080200; +$plugin->version = 2025080700; $plugin->maturity = MATURITY_STABLE; -$plugin->release = '2.1'; -$plugin->requires = 2022041200; // Moodle 4.1 (LTS) +$plugin->release = '2.2'; +$plugin->requires = 2022041200; // Moodle 4.1 (LTS). $plugin->phpversion = '7.4.0';