MDL-45170 files: check other draftfile areas when processing
authorCharles Fulton <fultonc@lafayette.edu>
Wed, 27 Jun 2018 20:32:50 +0000 (16:32 -0400)
committerJun Pataleta <jun@moodle.com>
Wed, 8 Aug 2018 08:15:23 +0000 (16:15 +0800)
lib/filelib.php
lib/tests/filelib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php

index 2d189a3..d56b821 100644 (file)
@@ -870,6 +870,127 @@ function file_remove_editor_orphaned_files($editor) {
     }
 }
 
+/**
+ * Finds all draft areas used in a textarea and copies the files into the primary textarea. If a user copies and pastes
+ * content from another draft area it's possible for a single textarea to reference multiple draft areas.
+ *
+ * @category files
+ * @param int $draftitemid the id of the primary draft area.
+ * @param int $usercontextid the user's context id.
+ * @param string $text some html content that needs to have files copied to the correct draft area.
+ * @param bool $forcehttps force https urls.
+ *
+ * @return string $text html content modified with new draft links
+ */
+function file_merge_draft_areas($draftitemid, $usercontextid, $text, $forcehttps=false) {
+    global $CFG;
+
+    if (is_null($text)) {
+        return null;
+    }
+
+    $urls = extract_draft_file_urls_from_text($text, $forcehttps, $usercontextid, 'user', 'draft');
+
+    // No draft areas to rewrite.
+    if (empty($urls)) {
+        return $text;
+    }
+
+    foreach ($urls as $url) {
+        // Do not process the "home" draft area.
+        if ($url['itemid'] == $draftitemid) {
+            continue;
+        }
+
+        // Decode the filename.
+        $filename = urldecode($url['filename']);
+
+        // Copy the file.
+        file_copy_file_to_file_area($url, $filename, $draftitemid);
+
+        // Rewrite draft area.
+        $text = file_replace_file_area_in_text($url, $draftitemid, $text, $forcehttps);
+    }
+    return $text;
+}
+
+/**
+ * Rewrites a file area in arbitrary text.
+ *
+ * @param array $file General information about the file.
+ * @param int $newid The new file area itemid.
+ * @param string $text The text to rewrite.
+ * @param bool $forcehttps force https urls.
+ * @return string The rewritten text.
+ */
+function file_replace_file_area_in_text($file, $newid, $text, $forcehttps = false) {
+    global $CFG;
+
+    $wwwroot = $CFG->wwwroot;
+    if ($forcehttps) {
+        $wwwroot = str_replace('http://', 'https://', $wwwroot);
+    }
+
+    $search = [
+        $wwwroot,
+        $file['urlbase'],
+        $file['contextid'],
+        $file['component'],
+        $file['filearea'],
+        $file['itemid'],
+        $file['filename']
+    ];
+    $replace = [
+        $wwwroot,
+        $file['urlbase'],
+        $file['contextid'],
+        $file['component'],
+        $file['filearea'],
+        $newid,
+        $file['filename']
+    ];
+
+    $text = str_ireplace( implode('/', $search), implode('/', $replace), $text);
+    return $text;
+}
+
+/**
+ * Copies a file from one file area to another.
+ *
+ * @param array $file Information about the file to be copied.
+ * @param string $filename The filename.
+ * @param int $itemid The new file area.
+ */
+function file_copy_file_to_file_area($file, $filename, $itemid) {
+    $fs = get_file_storage();
+
+    // Load the current file in the old draft area.
+    $fileinfo = array(
+        'component' => $file['component'],
+        'filearea' => $file['filearea'],
+        'itemid' => $file['itemid'],
+        'contextid' => $file['contextid'],
+        'filepath' => '/',
+        'filename' => $filename
+    );
+    $oldfile = $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'],
+        $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);
+    $newfileinfo = array(
+        'component' => $file['component'],
+        'filearea' => $file['filearea'],
+        'itemid' => $itemid,
+        'contextid' => $file['contextid'],
+        'filepath' => '/',
+        'filename' => $filename
+    );
+
+    // Check if the file exists.
+    if ( ! $fs->file_exists($newfileinfo['contextid'], $newfileinfo['component'], $newfileinfo['filearea'],
+        $newfileinfo['itemid'], $newfileinfo['filepath'], $newfileinfo['filename']) ) {
+            $newfile = $fs->create_file_from_storedfile($newfileinfo, $oldfile);
+    }
+}
+
 /**
  * Saves files from a draft file area to a real one (merging the list of files).
  * Can rewrite URLs in some content at the same time if desired.
@@ -915,6 +1036,10 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
         $allowreferences = false;
     }
 
+    // Check if the user has copy-pasted from other draft areas. Those files will be located in different draft
+    // areas and need to be copied into the current draft area.
+    $text = file_merge_draft_areas($draftitemid, $usercontext->id, $text, $forcehttps);
+
     // Check if the draft area has exceeded the authorised limit. This should never happen as validation
     // should have taken place before, unless the user is doing something nauthly. If so, let's just not save
     // anything at all in the next area.
index a6ff45f..640c3d7 100644 (file)
@@ -1473,6 +1473,43 @@ EOF;
         $this->assertEquals($fourthrecord['filename'], $allfiles[3]->filename);
         $this->assertEquals($fifthrecord['filename'], $allfiles[4]->filename);
     }
+
+    public function test_file_copy_file_to_file_area() {
+        // Create two files in different draft areas but owned by the same user.
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $filerecord = ['filename'  => 'file1.png', 'itemid' => file_get_unused_draft_itemid()];
+        $file1 = self::create_draft_file($filerecord);
+        $filerecord = ['filename'  => 'file2.png', 'itemid' => file_get_unused_draft_itemid()];
+        $file2 = self::create_draft_file($filerecord);
+
+        // Confirm one file in each draft area.
+        $fs = get_file_storage();
+        $usercontext = context_user::instance($USER->id);
+        $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file1->get_itemid(), 'itemid', 0);
+        $this->assertCount(1, $draftfiles);
+        $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file2->get_itemid(), 'itemid', 0);
+        $this->assertCount(1, $draftfiles);
+
+        // Create file record.
+        $filerecord = [
+            'component' => $file2->get_component(),
+            'filearea' => $file2->get_filearea(),
+            'itemid' => $file2->get_itemid(),
+            'contextid' => $file2->get_contextid(),
+            'filepath' => '/',
+            'filename' => $file2->get_filename()
+        ];
+
+        // Copy file2 into file1's draft area.
+        $newfile = file_copy_file_to_file_area($filerecord, $file2->get_filename(), $file1->get_itemid());
+        $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file1->get_itemid(), 'itemid', 0);
+        $this->assertCount(2, $draftfiles);
+        $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file2->get_itemid(), 'itemid', 0);
+        $this->assertCount(1, $draftfiles);
+    }
 }
 
 /**
index 5c3d4d4..c27b9af 100644 (file)
@@ -887,4 +887,52 @@ EXPECTED;
             $_GET = $currentget;
         }
     }
+
+    /**
+     * Tests for extract_draft_file_urls_from_text() function.
+     */
+    public function test_extract_draft_file_urls_from_text() {
+        global $CFG;
+
+        $url1 = "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999999/test1.jpg";
+        $url2 = "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999998/test2.jpg";
+
+        $html = "<p>This is a test.</p><p><img src=\"${url1}\" alt=\"\" role=\"presentation\"></p>
+                <br>Test content.<p></p><p><img src=\"{$url2}\" alt=\"\" width=\"2048\" height=\"1536\"
+                role=\"presentation\" class=\"img-responsive atto_image_button_text-bottom\"><br></p>";
+        $draftareas = array(
+            array(
+                'urlbase' => 'draftfile.php',
+                'contextid' => '5',
+                'component' => 'user',
+                'filearea' => 'draft',
+                'itemid' => '99999999',
+                'filename' => 'test1.jpg',
+                0 => "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999999/test1.jpg",
+                1 => 'draftfile.php',
+                2 => '5',
+                3 => 'user',
+                4 => 'draft',
+                5 => '99999999',
+                6 => 'test1.jpg'
+            ),
+            array(
+                'urlbase' => 'draftfile.php',
+                'contextid' => '5',
+                'component' => 'user',
+                'filearea' => 'draft',
+                'itemid' => '99999998',
+                'filename' => 'test2.jpg',
+                0 => "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999998/test2.jpg",
+                1 => 'draftfile.php',
+                2 => '5',
+                3 => 'user',
+                4 => 'draft',
+                5 => '99999998',
+                6 => 'test2.jpg'
+            )
+        );
+        $extracteddraftareas = extract_draft_file_urls_from_text($html, 5, 'user', 'draft');
+        $this->assertEquals($draftareas, $extracteddraftareas);
+    }
 }
index fb3dee5..f1ad454 100644 (file)
@@ -56,6 +56,13 @@ information provided here is intended especially for developers.
   hard-coded. Instead, the setting $CFG->showuseridentity should be always respected, which has always been the default
   behaviour (MDL-59847).
 
+* New functions to support the merging of user draft areas from the interface; see MDL-45170 for details:
+
+- file_copy_file_to_file_area()
+- file_merge_draft_areas()
+- file_replace_file_area_in_text()
+- extract_draft_file_urls_from_text()
+
 === 3.5 ===
 
 * There is a new privacy API that every subsystem and plugin has to implement so that the site can become GDPR
index ece1f68..0c76279 100644 (file)
@@ -2029,6 +2029,63 @@ function content_to_text($content, $contentformat) {
     return trim($content, "\r\n ");
 }
 
+/**
+ * Factory method for extracting draft file links from arbitrary text using regular expressions. Only text
+ * is required; other file fields may be passed to filter.
+ *
+ * @param string $text Some html content.
+ * @param bool $forcehttps force https urls.
+ * @param int $contextid This parameter and the next three identify the file area to save to.
+ * @param string $component The component name.
+ * @param string $filearea The filearea.
+ * @param int $itemid The item id for the filearea.
+ * @param string $filename The specific filename of the file.
+ * @return array
+ */
+function extract_draft_file_urls_from_text($text, $forcehttps = false, $contextid = null, $component = null,
+        $filearea = null, $itemid = null, $filename = null) {
+    global $CFG;
+
+    $wwwroot = $CFG->wwwroot;
+    if ($forcehttps) {
+        $wwwroot = str_replace('http://', 'https://', $wwwroot);
+    }
+    $urlstring = '/' . preg_quote($wwwroot, '/');
+
+    $urlbase = preg_quote('draftfile.php');
+    $urlstring .= "\/(?<urlbase>{$urlbase})";
+
+    if (is_null($contextid)) {
+        $contextid = '[0-9]+';
+    }
+    $urlstring .= "\/(?<contextid>{$contextid})";
+
+    if (is_null($component)) {
+        $component = '[a-z_]+';
+    }
+    $urlstring .= "\/(?<component>{$component})";
+
+    if (is_null($filearea)) {
+        $filearea = '[a-z_]+';
+    }
+    $urlstring .= "\/(?<filearea>{$filearea})";
+
+    if (is_null($itemid)) {
+        $itemid = '[0-9]+';
+    }
+    $urlstring .= "\/(?<itemid>{$itemid})";
+
+    // Filename matching magic based on file_rewrite_urls_to_pluginfile().
+    if (is_null($filename)) {
+        $filename = '[^\'\",&<>|`\s:\\\\]+';
+    }
+    $urlstring .= "\/(?<filename>{$filename})/";
+
+    // Regular expression which matches URLs and returns their components.
+    preg_match_all($urlstring, $text, $urls, PREG_SET_ORDER);
+    return $urls;
+}
+
 /**
  * This function will highlight search words in a given string
  *